diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index d8e8ae8b..6c69ddcb 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -3,6 +3,8 @@ import java.io.IOException plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.serialization) } fun String.runCommand(workingDir: File = File(".")): String? { @@ -61,7 +63,7 @@ android { buildTypes { release { - isMinifyEnabled = false + isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" @@ -118,6 +120,7 @@ android { buildConfig = true dataBinding = true viewBinding = true + compose = true } kotlinOptions { jvmTarget = "17" @@ -125,18 +128,26 @@ android { } dependencies { - implementation(libs.androidx.datastore.rxjava3) - implementation(libs.androidx.datastore.preferences) - implementation(libs.androidx.datastore) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.kotlinx.serialization.json) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) implementation(libs.okhttp) + implementation(libs.okhttp.coroutines) implementation(libs.androidx.appcompat) implementation(libs.material) - implementation(libs.androidx.constraintlayout) - implementation(libs.androidx.swiperefreshlayout) implementation(libs.androidx.navigation.fragment) + implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.ui) implementation(libs.androidx.legacy.support.v4) - implementation(libs.androidx.lifecycle.livedata.ktx) implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.core.ktx) testImplementation(libs.junit) diff --git a/src/android/app/proguard-rules.pro b/src/android/app/proguard-rules.pro index 481bb434..e9caf635 100644 --- a/src/android/app/proguard-rules.pro +++ b/src/android/app/proguard-rules.pro @@ -1,21 +1 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +-dontobfuscate diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index f64f0df7..dc2d8fa3 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,6 @@ xmlns:tools="http://schemas.android.com/tools"> - + + + + + + + + + + + @@ -51,19 +72,6 @@ --> - - - - - - - - GetStaticMethodID(*m_fileUtilClass, "openContentUri", "(Ljava/lang/String;)I"); m_listFilesMid = env->GetStaticMethodID(*m_fileUtilClass, "listFiles", "(Ljava/lang/String;)[Ljava/lang/String;"); m_isDirectoryMid = env->GetStaticMethodID(*m_fileUtilClass, "isDirectory", "(Ljava/lang/String;)Z"); diff --git a/src/android/app/src/main/cpp/AndroidGameTitleLoadedCallback.h b/src/android/app/src/main/cpp/AndroidGameTitleLoadedCallback.h index 1f9175e4..31504dc6 100644 --- a/src/android/app/src/main/cpp/AndroidGameTitleLoadedCallback.h +++ b/src/android/app/src/main/cpp/AndroidGameTitleLoadedCallback.h @@ -3,7 +3,7 @@ #include "GameTitleLoader.h" #include "JNIUtils.h" #include - +// TODO: Refactor this: class AndroidGameTitleLoadedCallback : public GameTitleLoadedCallback { jmethodID m_onGameTitleLoadedMID; diff --git a/src/android/app/src/main/cpp/CMakeLists.txt b/src/android/app/src/main/cpp/CMakeLists.txt index 176d5681..3dd53f86 100644 --- a/src/android/app/src/main/cpp/CMakeLists.txt +++ b/src/android/app/src/main/cpp/CMakeLists.txt @@ -6,6 +6,7 @@ add_library(CemuAndroid SHARED GameTitleLoader.cpp Image.cpp JNIUtils.cpp + NativeActiveSettings.cpp NativeEmulation.cpp NativeGameTitles.cpp NativeGraphicPacks.cpp diff --git a/src/android/app/src/main/cpp/NativeActiveSettings.cpp b/src/android/app/src/main/cpp/NativeActiveSettings.cpp new file mode 100644 index 00000000..518598f4 --- /dev/null +++ b/src/android/app/src/main/cpp/NativeActiveSettings.cpp @@ -0,0 +1,8 @@ +#include "JNIUtils.h" +#include "config/ActiveSettings.h" + +extern "C" [[maybe_unused]] JNIEXPORT jstring JNICALL +Java_info_cemu_cemu_nativeinterface_NativeActiveSettings_getMLCPath(JNIEnv* env, [[maybe_unused]] jclass clazz) +{ + return env->NewStringUTF(ActiveSettings::GetMlcPath().c_str()); +} diff --git a/src/android/app/src/main/cpp/NativeGameTitles.cpp b/src/android/app/src/main/cpp/NativeGameTitles.cpp index 7f38e5e0..f41e2444 100644 --- a/src/android/app/src/main/cpp/NativeGameTitles.cpp +++ b/src/android/app/src/main/cpp/NativeGameTitles.cpp @@ -1,3 +1,4 @@ +#include #include "JNIUtils.h" #include "Cafe/GameProfile/GameProfile.h" #include "GameTitleLoader.h" @@ -142,3 +143,185 @@ Java_info_cemu_cemu_nativeinterface_NativeGameTitles_getInstalledGamesTitleIds(J { return JNIUtils::createJavaLongArrayList(env, CafeTitleList::GetAllTitleIds()); } + +class SaveListCallback +{ + private: + uint64 m_callbackIdSaveList; + JNIUtils::Scopedjobject m_saveListCallbackObj; + jmethodID m_onSaveDiscoveredMID; + JNIUtils::Scopedjclass m_saveDataClass; + jmethodID m_saveDataConstructorMID; + + void HandleSaveListCallback(CafeSaveListCallbackEvent* evt) + { + if (evt->eventType != CafeSaveListCallbackEvent::TYPE::SAVE_DISCOVERED) + return; + + ParsedMetaXml* metaInfo = evt->saveInfo->GetMetaInfo(); + if (!metaInfo) + return; + auto& saveInfo = *evt->saveInfo; + auto locationUID = std::hash()(metaInfo->GetTitleId()); + std::string name = metaInfo->GetLongName(GetConfig().console_language.GetValue()); + const auto nl = name.find(L'\n'); + if (nl != std::string::npos) + name.replace(nl, 1, " - "); + + JNIUtils::ScopedJNIENV env; + jstring nameJava = env->NewStringUTF(name.c_str()); + jobject pathJava = env->NewStringUTF(saveInfo.GetPath().c_str()); + jobject saveData = env->NewObject( + *m_saveDataClass, + m_saveDataConstructorMID, + nameJava, + pathJava, + metaInfo->GetTitleId(), + locationUID, + metaInfo->GetTitleVersion(), + metaInfo->GetRegion()); + env->CallVoidMethod(*m_saveListCallbackObj, m_onSaveDiscoveredMID, saveData); + env->DeleteLocalRef(saveData); + env->DeleteLocalRef(nameJava); + env->DeleteLocalRef(pathJava); + } + + public: + SaveListCallback(jobject saveListCallbackObject) + { + JNIUtils::ScopedJNIENV env; + m_saveListCallbackObj = JNIUtils::Scopedjobject(saveListCallbackObject); + JNIUtils::Scopedjclass saveCallbacksClass{"info/cemu/cemu/nativeinterface/NativeGameTitles$SaveListCallback"}; + m_onSaveDiscoveredMID = env->GetMethodID(*saveCallbacksClass, "onSaveDiscovered", "(Linfo/cemu/cemu/nativeinterface/NativeGameTitles$SaveData;)V"); + m_saveDataClass = JNIUtils::Scopedjclass("info/cemu/cemu/nativeinterface/NativeGameTitles$SaveData"); + m_saveDataConstructorMID = env->GetMethodID(*m_saveDataClass, "", "(Ljava/lang/String;Ljava/lang/String;JJSI)V"); + m_callbackIdSaveList = CafeSaveList::RegisterCallback( + [](CafeSaveListCallbackEvent* evt, void* ctx) { + static_cast(ctx)->HandleSaveListCallback(evt); + }, + this); + } + ~SaveListCallback() + { + CafeSaveList::UnregisterCallback(m_callbackIdSaveList); + } +}; + +class TitleListCallbacks +{ + private: + JNIUtils::Scopedjobject m_titleListCallbacksObj; + jmethodID m_onTitleDiscoveredMID; + jmethodID m_onTitleRemovedMID; + jmethodID m_titleDataConstructorMID; + JNIUtils::Scopedjclass m_titleDataClass; + uint64 m_callbackIdTitleList; + + void OnTitleDiscovered(TitleInfo& titleInfo) + { + if (titleInfo.IsCached()) + return; // the title list only displays non-cached entries + if (titleInfo.IsSystemDataTitle()) + return; // don't show system data titles for now + + JNIUtils::ScopedJNIENV env; + + ParsedMetaXml* metaInfo = titleInfo.GetMetaInfo(); + std::string name = metaInfo->GetLongName(GetConfig().console_language.GetValue()); + const auto nl = name.find(L'\n'); + if (nl != std::string::npos) + name.replace(nl, 1, " - "); + + jobject nameJava = env->NewStringUTF(name.c_str()); + jobject pathJava = env->NewStringUTF(titleInfo.GetPath().c_str()); + jobject titleData = env->NewObject( + *m_titleDataClass, + m_titleDataConstructorMID, + nameJava, + pathJava, + titleInfo.GetAppTitleId(), + titleInfo.GetUID(), + titleInfo.GetAppTitleVersion(), + metaInfo->GetRegion(), + titleInfo.GetTitleType(), + titleInfo.GetFormat()); + + env->CallVoidMethod(*m_titleListCallbacksObj, m_onTitleDiscoveredMID, titleData); + + env->DeleteLocalRef(titleData); + env->DeleteLocalRef(nameJava); + env->DeleteLocalRef(pathJava); + } + + void OnTitleRemoved(TitleInfo& titleInfo) + { + JNIUtils::ScopedJNIENV()->CallVoidMethod(*m_titleListCallbacksObj, m_onTitleRemovedMID, titleInfo.GetUID()); + } + + void HandleTitleListCallback(CafeTitleListCallbackEvent* evt) + { + if (evt->eventType != CafeTitleListCallbackEvent::TYPE::TITLE_DISCOVERED && + evt->eventType != CafeTitleListCallbackEvent::TYPE::TITLE_REMOVED) + return; + + if (evt->eventType == CafeTitleListCallbackEvent::TYPE::TITLE_DISCOVERED) + { + OnTitleDiscovered(*evt->titleInfo); + } + else if (evt->eventType == CafeTitleListCallbackEvent::TYPE::TITLE_REMOVED) + { + OnTitleRemoved(*evt->titleInfo); + } + } + + public: + TitleListCallbacks(jobject titleListCallbacks) + { + JNIUtils::ScopedJNIENV env; + m_titleListCallbacksObj = JNIUtils::Scopedjobject(titleListCallbacks); + jclass titleCallbacksClass = env->FindClass("info/cemu/cemu/nativeinterface/NativeGameTitles$TitleListCallbacks"); + m_onTitleDiscoveredMID = env->GetMethodID(titleCallbacksClass, "onTitleDiscovered", "(Linfo/cemu/cemu/nativeinterface/NativeGameTitles$TitleData;)V"); + m_onTitleRemovedMID = env->GetMethodID(titleCallbacksClass, "onTitleRemoved", "(J)V"); + env->DeleteLocalRef(titleCallbacksClass); + m_titleDataClass = JNIUtils::Scopedjclass("info/cemu/cemu/nativeinterface/NativeGameTitles$TitleData"); + m_titleDataConstructorMID = env->GetMethodID(*m_titleDataClass, "", "(Ljava/lang/String;Ljava/lang/String;JJSIII)V"); + m_callbackIdTitleList = CafeTitleList::RegisterCallback( + [](CafeTitleListCallbackEvent* evt, void* ctx) { + static_cast(ctx)->HandleTitleListCallback(evt); + }, + this); + } + + ~TitleListCallbacks() + { + CafeTitleList::UnregisterCallback(m_callbackIdTitleList); + } +}; + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +Java_info_cemu_cemu_nativeinterface_NativeGameTitles_refreshCafeTitleList([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) +{ + CafeTitleList::Refresh(); +} + +std::unique_ptr s_titleListCallbacks; + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +Java_info_cemu_cemu_nativeinterface_NativeGameTitles_setTitleListCallbacks([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jobject title_list_callbacks) +{ + if (title_list_callbacks == nullptr) + s_titleListCallbacks = nullptr; + else + s_titleListCallbacks = std::make_unique(title_list_callbacks); +} + +std::unique_ptr s_saveListCallback; + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +Java_info_cemu_cemu_nativeinterface_NativeGameTitles_setSaveListCallback([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jobject save_list_callback) +{ + if (save_list_callback == nullptr) + s_saveListCallback = nullptr; + else + s_saveListCallback = std::make_unique(save_list_callback); +} \ No newline at end of file diff --git a/src/android/app/src/main/cpp/NativeSettings.cpp b/src/android/app/src/main/cpp/NativeSettings.cpp index da2cb840..e4f1c4de 100644 --- a/src/android/app/src/main/cpp/NativeSettings.cpp +++ b/src/android/app/src/main/cpp/NativeSettings.cpp @@ -1,4 +1,5 @@ #include "JNIUtils.h" +#include "audio/IAudioAPI.h" #include "config/CemuConfig.h" extern "C" [[maybe_unused]] JNIEXPORT jint JNICALL @@ -223,13 +224,13 @@ Java_info_cemu_cemu_nativeinterface_NativeSettings_setAsyncShaderCompile([[maybe } extern "C" [[maybe_unused]] JNIEXPORT jint JNICALL -Java_info_cemu_cemu_nativeinterface_NativeSettings_getVSyncMode([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) +Java_info_cemu_cemu_nativeinterface_NativeSettings_getVsyncMode([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { return g_config.data().vsync; } extern "C" [[maybe_unused]] JNIEXPORT void JNICALL -Java_info_cemu_cemu_nativeinterface_NativeSettings_setVSyncMode([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint vsync_mode) +Java_info_cemu_cemu_nativeinterface_NativeSettings_setVsyncMode([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint vsync_mode) { g_config.data().vsync = vsync_mode; g_config.Save(); @@ -248,7 +249,7 @@ Java_info_cemu_cemu_nativeinterface_NativeSettings_setUpscalingFilter([[maybe_un g_config.Save(); } -extern "C" [[maybe_unused]] JNIEXPORT jboolean JNICALL +extern "C" [[maybe_unused]] JNIEXPORT jint JNICALL Java_info_cemu_cemu_nativeinterface_NativeSettings_getUpscalingFilter([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { return g_config.data().upscale_filter; @@ -261,7 +262,7 @@ Java_info_cemu_cemu_nativeinterface_NativeSettings_setDownscalingFilter([[maybe_ g_config.Save(); } -extern "C" [[maybe_unused]] JNIEXPORT jboolean JNICALL +extern "C" [[maybe_unused]] JNIEXPORT jint JNICALL Java_info_cemu_cemu_nativeinterface_NativeSettings_getDownscalingFilter([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { return g_config.data().downscale_filter; @@ -274,7 +275,7 @@ Java_info_cemu_cemu_nativeinterface_NativeSettings_setFullscreenScaling([[maybe_ g_config.Save(); } -extern "C" [[maybe_unused]] JNIEXPORT jboolean JNICALL +extern "C" [[maybe_unused]] JNIEXPORT jint JNICALL Java_info_cemu_cemu_nativeinterface_NativeSettings_getFullscreenScaling([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { return g_config.data().fullscreen_scaling; @@ -338,13 +339,14 @@ Java_info_cemu_cemu_nativeinterface_NativeSettings_setAudioDeviceVolume([[maybe_ extern "C" [[maybe_unused]] JNIEXPORT jint JNICALL Java_info_cemu_cemu_nativeinterface_NativeSettings_getAudioLatency([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { - return g_config.data().audio_delay; + return g_config.data().audio_delay * 12; } extern "C" [[maybe_unused]] JNIEXPORT void JNICALL Java_info_cemu_cemu_nativeinterface_NativeSettings_setAudioLatency([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint latency) { - g_config.data().audio_delay = latency; + g_config.data().audio_delay = latency / 12; + IAudioAPI::SetAudioDelay(g_config.data().audio_delay); g_config.Save(); } 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 508a6085..703a1f53 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 @@ -13,7 +13,7 @@ import java.io.StringWriter class CemuApplication : Application() { init { - application = this + Application = this } val internalFolder: File @@ -27,8 +27,8 @@ class CemuApplication : Application() { override fun onCreate() { super.onCreate() - if (defaultUncaughtExceptionHandler == null) { - defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() + if (DefaultUncaughtExceptionHandler == null) { + DefaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() } Thread.setDefaultUncaughtExceptionHandler { thread: Thread, exception: Throwable -> val stringWriter = StringWriter() @@ -36,7 +36,7 @@ class CemuApplication : Application() { exception.printStackTrace(printWriter) val stacktrace = stringWriter.toString() crashLog(stacktrace) - defaultUncaughtExceptionHandler!!.uncaughtException( + DefaultUncaughtExceptionHandler!!.uncaughtException( thread, exception ) @@ -54,10 +54,10 @@ class CemuApplication : Application() { System.loadLibrary("CemuAndroid") } - private var defaultUncaughtExceptionHandler: Thread.UncaughtExceptionHandler? = null + private var DefaultUncaughtExceptionHandler: Thread.UncaughtExceptionHandler? = null @JvmStatic - lateinit var application: CemuApplication + lateinit var Application: CemuApplication private set } } 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 eb81889f..eab58b71 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 @@ -1,13 +1,185 @@ package info.cemu.cemu +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import info.cemu.cemu.databinding.ActivityMainBinding +import android.provider.DocumentsContract +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import info.cemu.cemu.about.AboutCemuActivity +import info.cemu.cemu.emulation.EmulationActivity +import info.cemu.cemu.features.DocumentsProvider +import info.cemu.cemu.gamelist.GameDetailsScreen +import info.cemu.cemu.gamelist.GameProfileEditScreen +import info.cemu.cemu.gamelist.GameViewModel +import info.cemu.cemu.gamelist.GamesListScreen +import info.cemu.cemu.gamelist.GamesListScreenActions +import info.cemu.cemu.graphicpacks.GraphicPacksActivity +import info.cemu.cemu.guicore.ActivityContent +import info.cemu.cemu.nativeinterface.NativeGameTitles.Game +import info.cemu.cemu.settings.SettingsActivity +import kotlinx.serialization.Serializable -class MainActivity : AppCompatActivity() { +class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) + setContent { + ActivityContent { + MainNav() + } + } } -} \ No newline at end of file +} + +private fun startGame(context: Context, game: Game) { + context.startActivity( + Intent( + context, + EmulationActivity::class.java + ).apply { + putExtra(EmulationActivity.EXTRA_LAUNCH_PATH, game.path) + } + ) +} + +sealed class MainRoutes { + @Serializable + object GamesRoute + + @Serializable + object GameDetailsRoute + + @Serializable + object GameProfileEditRoute +} + + +@Composable +fun MainNav() { + val navController = rememberNavController() + val selectedGameViewModel: GameViewModel = viewModel() + fun navigateBack() { + navController.popBackStack() + } + NavHost(navController = navController, startDestination = MainRoutes.GamesRoute) { + composable { + val context = LocalContext.current + GamesListScreen( + selectedGameViewModel = selectedGameViewModel, + gameListActions = GamesListScreenActions( + goToGameEditProfile = { navController.navigate(MainRoutes.GameProfileEditRoute) }, + startGame = { startGame(context, it) }, + goToGameDetails = { navController.navigate(MainRoutes.GameDetailsRoute) }, + ), + toolbarActions = { + GameListToolBarActions() + } + ) + } + composable { + GameDetailsScreen( + selectedGameViewModel = selectedGameViewModel, + navigateBack = ::navigateBack, + ) + } + composable { + GameProfileEditScreen( + selectedGameViewModel = selectedGameViewModel, + navigateBack = ::navigateBack, + ) + } + } +} + +@Composable +fun GameListToolBarActions() { + var expandMenu by remember { mutableStateOf(false) } + val context = LocalContext.current + IconButton( + modifier = Modifier.padding(end = 8.dp), + onClick = { expandMenu = true }, + ) { + IconButton( + onClick = { expandMenu = true } + ) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = "More options" + ) + + } + } + DropdownMenu( + expanded = expandMenu, + onDismissRequest = { expandMenu = false } + ) { + DropdownMenuItem( + onClick = { context.goToActivity(SettingsActivity::class.java) }, + text = { Text(stringResource(R.string.settings)) }, + ) + DropdownMenuItem( + onClick = { context.goToActivity(GraphicPacksActivity::class.java) }, + text = { Text(stringResource(R.string.graphic_packs)) }, + ) + DropdownMenuItem( + onClick = { openCemuFolder(context) }, + text = { Text(stringResource(R.string.open_cemu_folder)) }, + ) + DropdownMenuItem( + onClick = { context.goToActivity(AboutCemuActivity::class.java) }, + text = { Text(stringResource(R.string.about_cemu)) }, + ) + } +} + +private fun Context.goToActivity(activityClass: Class) { + startActivity( + Intent( + this, + activityClass, + ) + ) +} + +fun openCemuFolder(context: Context) { + try { + Intent(Intent.ACTION_VIEW).apply { + addCategory(Intent.CATEGORY_DEFAULT) + data = DocumentsContract.buildRootUri( + DocumentsProvider.AUTHORITY, + DocumentsProvider.ROOT_ID + ) + addFlags( + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION + or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + context.startActivity(this@apply) + } + } catch (activityNotFoundException: ActivityNotFoundException) { + Toast.makeText(context, R.string.failed_to_open_cemu_folder, Toast.LENGTH_LONG).show() + } +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/about/AboutCemuActivity.kt b/src/android/app/src/main/java/info/cemu/cemu/about/AboutCemuActivity.kt new file mode 100644 index 00000000..7b31d424 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/about/AboutCemuActivity.kt @@ -0,0 +1,18 @@ +package info.cemu.cemu.about + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import info.cemu.cemu.guicore.ActivityContent + + +class AboutCemuActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + ActivityContent { + AboutCemuScreen(::onNavigateUp) + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..64d473f0 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/about/AboutCemuScreen.kt @@ -0,0 +1,151 @@ +package info.cemu.cemu.about + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withLink +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import info.cemu.cemu.BuildConfig +import info.cemu.cemu.R +import info.cemu.cemu.guicore.ScreenContent + +@Composable +fun AboutCemuScreen(navigateBack: () -> Unit) { + ScreenContent( + appBarText = stringResource(R.string.about_cemu), + navigateBack = navigateBack, + contentModifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentVerticalArrangement = Arrangement.spacedBy(16.dp), + ) { + AboutSection { + Text( + text = stringResource(R.string.cemu), + fontSize = 32.sp, + ) + Text( + text = stringResource(R.string.cemu_version, BuildConfig.VERSION_NAME), + fontSize = 18.sp, + ) + Text( + text = stringResource( + R.string.original_authors, + stringResource(R.string.cemu_original_authors) + ), + fontSize = 18.sp, + ) + CemuWebsite() + } + AboutSection { + Text( + text = stringResource(R.string.cemu_description), + fontSize = 18.sp + ) + } + AboutSection { + Text( + text = stringResource(R.string.used_libraries), + fontSize = 18.sp, + ) + UsedLibraries.forEach { + Library(it) + } + Text(stringResource(R.string.ih264_library_description)) + } + } +} + +@Composable +fun CemuWebsite() { + Text( + buildAnnotatedString { + append(stringResource(R.string.website)) + append(" ") + withLink( + LinkAnnotation.Url( + CEMU_WEBSITE, + TextLinkStyles( + style = SpanStyle( + color = MaterialTheme.colorScheme.onSurfaceVariant, + textDecoration = TextDecoration.Underline, + ), + ) + ) + ) { + append(CEMU_WEBSITE) + } + } + ) +} + +@Composable +fun AboutSection(content: @Composable ColumnScope.() -> Unit) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + ), + modifier = Modifier.fillMaxWidth(), + ) { + SelectionContainer { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(16.dp), + content = content, + ) + } + } +} + +@Composable +fun Library(nameLinkPair: Pair) { + val (name, link) = nameLinkPair + Text( + buildAnnotatedString { + append(name) + append(" (") + withLink( + LinkAnnotation.Url( + link, + TextLinkStyles( + style = SpanStyle( + color = MaterialTheme.colorScheme.onSurfaceVariant, + textDecoration = TextDecoration.Underline, + ), + ) + ) + ) { + append(link) + } + append(")") + } + ) +} + +private val UsedLibraries: List> = listOf( + "zlib" to "https://www.zlib.net", + "OpenSSL" to "https://www.openssl.org", + "libcurl" to "https://curl.haxx.se/libcurl", + "imgui" to "https://github.com/ocornut/imgui", + "fontawesome" to "https://github.com/FortAwesome/Font-Awesome", + "boost" to "https://www.boost.org", + "libusb" to "https://libusb.info", +) + +private const val CEMU_WEBSITE = "https://cemu.info" 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 93274a14..ff1ba9d4 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 @@ -7,6 +7,7 @@ import android.view.KeyEvent import android.view.MotionEvent import android.view.View import androidx.activity.OnBackPressedCallback +import androidx.annotation.Keep import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat @@ -17,7 +18,6 @@ import info.cemu.cemu.BuildConfig import info.cemu.cemu.R import info.cemu.cemu.databinding.ActivityEmulationBinding import info.cemu.cemu.emulation.EmulationFragment.OnEmulationErrorCallback -import info.cemu.cemu.emulation.EmulationTextInputEditText.OnTextChangedListener import info.cemu.cemu.input.InputManager import info.cemu.cemu.nativeinterface.NativeSwkbd.setCurrentInputText import java.util.Objects @@ -89,8 +89,8 @@ class EmulationActivity : AppCompatActivity() { val builder = MaterialAlertDialogBuilder(this) builder.setTitle(R.string.exit_confirmation_title) .setMessage(R.string.exit_confirm_message) - .setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> quit() } - .setNegativeButton(R.string.no) { dialog: DialogInterface, _: Int -> dialog.cancel() } + .setPositiveButton(R.string.yes) { _, _ -> quit() } + .setNegativeButton(R.string.no) { _, _ -> } .show() } @@ -102,10 +102,8 @@ class EmulationActivity : AppCompatActivity() { val builder = MaterialAlertDialogBuilder(this) builder.setTitle(R.string.error) .setMessage(emulationError) - .setNeutralButton( - R.string.quit - ) { dialog: DialogInterface, which: Int -> dialog.dismiss() } - .setOnDismissListener { dialog: DialogInterface? -> quit() } + .setNeutralButton(R.string.quit) { _, _ -> } + .setOnDismissListener { _ -> quit() } .show() } @@ -129,7 +127,7 @@ class EmulationActivity : AppCompatActivity() { /** * This method is called by swkbd using JNI. */ - @Suppress("unused") + @Keep @JvmStatic fun showEmulationTextInput(initialText: String?, maxLength: Int) { if (emulationActivityInstance == null || emulationActivityInstance!!.emulationTextInputDialog != null) { @@ -171,7 +169,7 @@ class EmulationActivity : AppCompatActivity() { /** * This method is called by swkbd using JNI. */ - @Suppress("unused") + @Keep @JvmStatic fun hideEmulationTextInput() { if (emulationActivityInstance?.emulationTextInputDialog == null) { diff --git a/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationFragment.kt b/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationFragment.kt index 460f0552..a9697005 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationFragment.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationFragment.kt @@ -22,7 +22,6 @@ import info.cemu.cemu.R import info.cemu.cemu.databinding.FragmentEmulationBinding import info.cemu.cemu.input.SensorManager import info.cemu.cemu.inputoverlay.InputOverlaySettingsProvider -import info.cemu.cemu.inputoverlay.InputOverlaySettingsProvider.OverlaySettings import info.cemu.cemu.inputoverlay.InputOverlaySurfaceView import info.cemu.cemu.nativeinterface.NativeEmulation import info.cemu.cemu.nativeinterface.NativeEmulation.clearSurface @@ -81,7 +80,7 @@ class EmulationFragment(private val launchPath: String) : Fragment(), surfaceHolder: SurfaceHolder, format: Int, width: Int, - height: Int + height: Int, ) { try { setSurfaceSize(width, height, isMainCanvas) @@ -115,7 +114,6 @@ class EmulationFragment(private val launchPath: String) : Fragment(), private var sensorManager: SensorManager? = null private var onEmulationErrorCallback: OnEmulationErrorCallback? = null private var hasEmulationError = false - private var overlaySettings: OverlaySettings? = null fun setOnEmulationErrorCallback(onEmulationErrorCallback: OnEmulationErrorCallback) { this.onEmulationErrorCallback = onEmulationErrorCallback @@ -128,7 +126,6 @@ class EmulationFragment(private val launchPath: String) : Fragment(), sensorManager = SensorManager(requireContext()) } sensorManager!!.setIsLandscape(resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) - overlaySettings = inputOverlaySettingsProvider.overlaySettings } override fun onConfigurationChanged(newConfig: Configuration) { @@ -248,7 +245,7 @@ class EmulationFragment(private val launchPath: String) : Fragment(), override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View { binding = FragmentEmulationBinding.inflate(inflater, container, false) inputOverlaySurfaceView = binding!!.inputOverlay @@ -282,12 +279,12 @@ class EmulationFragment(private val launchPath: String) : Fragment(), settingsMenu!!.setOnMenuItemClickListener(this@EmulationFragment) binding!!.emulationSettingsButton.setOnClickListener { v: View? -> settingsMenu!!.show() } val menu = settingsMenu!!.menu - menu.findItem(R.id.show_input_overlay).setChecked(overlaySettings!!.isOverlayEnabled) - if (!overlaySettings!!.isOverlayEnabled) { - menu.findItem(R.id.reset_inputs).setEnabled(false) - menu.findItem(R.id.edit_inputs).setEnabled(false) - inputOverlaySurfaceView!!.visibility = View.GONE - } +// menu.findItem(R.id.show_input_overlay).setChecked(overlaySettings!!.isOverlayEnabled) +// if (!overlaySettings!!.isOverlayEnabled) { +// menu.findItem(R.id.reset_inputs).setEnabled(false) +// menu.findItem(R.id.edit_inputs).setEnabled(false) +// inputOverlaySurfaceView!!.visibility = View.GONE +// } val mainCanvas = binding!!.mainCanvas try { val testSurfaceTexture = SurfaceTexture(0) @@ -315,7 +312,7 @@ class EmulationFragment(private val launchPath: String) : Fragment(), holder: SurfaceHolder, format: Int, width: Int, - height: Int + height: Int, ) { if (hasEmulationError) { return 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 6329cc4f..c4b61dee 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 @@ -9,14 +9,13 @@ import android.util.AttributeSet import android.view.inputmethod.EditorInfo import com.google.android.material.R import com.google.android.material.textfield.TextInputEditText -import info.cemu.cemu.nativeinterface.NativeSwkbd.onFinishedInputEdit -import info.cemu.cemu.nativeinterface.NativeSwkbd.onTextChanged +import info.cemu.cemu.nativeinterface.NativeSwkbd import java.util.regex.Pattern class EmulationTextInputEditText @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.editTextStyle + defStyleAttr: Int = R.attr.editTextStyle, ) : TextInputEditText(context, attrs, defStyleAttr) { fun appendFilter(inputFilter: InputFilter) { @@ -60,7 +59,7 @@ class EmulationTextInputEditText @JvmOverloads constructor( if (onTextChangedListener != null) { onTextChangedListener!!.onTextChanged(text) } - onTextChanged(text.toString()) + NativeSwkbd.onTextChanged(text.toString()) } override fun afterTextChanged(s: Editable) { @@ -77,7 +76,7 @@ class EmulationTextInputEditText @JvmOverloads constructor( } fun onFinishedEdit() { - onFinishedInputEdit() + NativeSwkbd.onFinishedInputEdit() } fun setOnTextChangedListener(onTextChangedListener: OnTextChangedListener) { diff --git a/src/android/app/src/main/java/info/cemu/cemu/features/DocumentsProvider.kt b/src/android/app/src/main/java/info/cemu/cemu/features/DocumentsProvider.kt index 48dc6f9d..be4eb7bc 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/features/DocumentsProvider.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/features/DocumentsProvider.kt @@ -22,45 +22,29 @@ import java.io.IOException import java.util.Objects class DocumentsProvider : DocumentsProvider() { - private var baseDirectory: File? = null - private val applicationName = - CemuApplication.application.applicationInfo.loadLabel(CemuApplication.application.packageManager) - .toString() - - private val DEFAULT_ROOT_PROJECTION = arrayOf( - DocumentsContract.Root.COLUMN_ROOT_ID, - DocumentsContract.Root.COLUMN_MIME_TYPES, - DocumentsContract.Root.COLUMN_FLAGS, - DocumentsContract.Root.COLUMN_ICON, - DocumentsContract.Root.COLUMN_TITLE, - DocumentsContract.Root.COLUMN_SUMMARY, - DocumentsContract.Root.COLUMN_DOCUMENT_ID, - DocumentsContract.Root.COLUMN_AVAILABLE_BYTES - ) - - private val DEFAULT_DOCUMENT_PROJECTION = arrayOf( - DocumentsContract.Document.COLUMN_DOCUMENT_ID, - DocumentsContract.Document.COLUMN_MIME_TYPE, - DocumentsContract.Document.COLUMN_DISPLAY_NAME, - DocumentsContract.Document.COLUMN_LAST_MODIFIED, - DocumentsContract.Document.COLUMN_FLAGS, - DocumentsContract.Document.COLUMN_SIZE - ) - - init { - try { - baseDirectory = CemuApplication.application.internalFolder.canonicalFile - } catch (e: IOException) { - throw RuntimeException(e) + private var _baseDirectory: File? = null + private val baseDirectory: File + get() { + if (_baseDirectory == null) { + try { + _baseDirectory = CemuApplication.Application.internalFolder.canonicalFile + } catch (e: IOException) { + throw RuntimeException(e) + } + } + return _baseDirectory!! } - } + + private val applicationName = + CemuApplication.Application.applicationInfo.loadLabel(CemuApplication.Application.packageManager) + .toString() override fun onCreate(): Boolean { return true } override fun queryRoots(projection: Array?): Cursor { - val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION) + val cursor = MatrixCursor(projection ?: DefaultRootProjection) cursor.newRow().add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT_ID) .add(DocumentsContract.Root.COLUMN_SUMMARY, null) .add( @@ -68,16 +52,16 @@ class DocumentsProvider : DocumentsProvider() { DocumentsContract.Root.FLAG_SUPPORTS_CREATE or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD ) .add(DocumentsContract.Root.COLUMN_TITLE, applicationName) - .add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocumentId(baseDirectory!!)) + .add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocumentId(baseDirectory)) .add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*") - .add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDirectory!!.freeSpace) + .add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDirectory.freeSpace) .add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher) return cursor } @Throws(FileNotFoundException::class) override fun queryDocument(documentId: String, projection: Array?): Cursor { - val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION) + val cursor = MatrixCursor(projection ?: DefaultDocumentProjection) includeFile(cursor, documentId, null) return cursor } @@ -139,11 +123,11 @@ class DocumentsProvider : DocumentsProvider() { continue } if (!file.delete()) { - throw FileNotFoundException("Couldn't delete file " + file.path) + throw FileNotFoundException("Couldn't delete file ${file.path}") } } if (!dirFile.delete()) { - throw FileNotFoundException("Couldn't delete file " + dirFile.path) + throw FileNotFoundException("Couldn't delete file ${dirFile.path}") } } @@ -177,10 +161,10 @@ class DocumentsProvider : DocumentsProvider() { try { if (!sourceFile.renameTo(destFile)) { - throw FileNotFoundException("Couldn't rename document from '" + sourceFile.name + "' to '" + destFile.name + "'") + throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}'") } } catch (exception: Exception) { - throw FileNotFoundException("Couldn't rename document from '" + sourceFile.name + "' to '" + destFile.name + "':" + exception.message) + throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to 'destFile.name': ${exception.message}") } return getDocumentId(destFile) @@ -206,7 +190,7 @@ class DocumentsProvider : DocumentsProvider() { } } } catch (exception: IOException) { - throw FileNotFoundException("Couldn't copy document '" + sourceDocumentId + "': " + exception.message) + throw FileNotFoundException("Couldn't copy document '$sourceDocumentId': ${exception.message}") } return getDocumentId(newFile) } @@ -233,7 +217,7 @@ class DocumentsProvider : DocumentsProvider() { projection: Array?, sortOrder: String ): Cursor { - val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION) + val cursor = MatrixCursor(projection ?: DefaultDocumentProjection) val parent = getFile(parentDocumentId) val files = parent.listFiles() ?: return cursor for (file in files) { @@ -306,18 +290,19 @@ class DocumentsProvider : DocumentsProvider() { } flags = (flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE or DocumentsContract.Document.FLAG_SUPPORTS_REMOVE) - val curorRowBuilder = - cursor.newRow().add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, localDocumentId) - .add( - DocumentsContract.Document.COLUMN_DISPLAY_NAME, - if (localFile == baseDirectory) applicationName else localFile.name - ) - .add(DocumentsContract.Document.COLUMN_SIZE, localFile.length()) - .add(DocumentsContract.Document.COLUMN_MIME_TYPE, getTypeForFile(localFile)) - .add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, localFile.lastModified()) - .add(DocumentsContract.Document.COLUMN_FLAGS, flags) - if (localFile == baseDirectory) { - curorRowBuilder.add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher) + cursor.newRow().apply { + add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, localDocumentId) + add( + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + if (localFile == baseDirectory) applicationName else localFile.name + ) + add(DocumentsContract.Document.COLUMN_SIZE, localFile.length()) + add(DocumentsContract.Document.COLUMN_MIME_TYPE, getTypeForFile(localFile)) + add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, localFile.lastModified()) + add(DocumentsContract.Document.COLUMN_FLAGS, flags) + if (localFile == baseDirectory) { + add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher) + } } } @@ -344,7 +329,7 @@ class DocumentsProvider : DocumentsProvider() { private fun getFile(documentId: String?): File { Objects.requireNonNull(documentId) if (documentId!!.startsWith(ROOT_ID)) { - val file = resolve(baseDirectory!!, documentId.substring(ROOT_ID.length + 1)) + val file = resolve(baseDirectory, documentId.substring(ROOT_ID.length + 1)) if (!file.exists()) { throw FileNotFoundException(file.absolutePath + " " + documentId + " not found") } @@ -355,11 +340,29 @@ class DocumentsProvider : DocumentsProvider() { } private fun getDocumentId(file: File): String { - return ROOT_ID + "/" + baseDirectory!!.toPath().relativize(file.toPath()).toString() + return ROOT_ID + "/" + baseDirectory.toPath().relativize(file.toPath()).toString() } companion object { const val ROOT_ID: String = "root" const val AUTHORITY: String = BuildConfig.APPLICATION_ID + ".provider" + private val DefaultRootProjection = arrayOf( + DocumentsContract.Root.COLUMN_ROOT_ID, + DocumentsContract.Root.COLUMN_MIME_TYPES, + DocumentsContract.Root.COLUMN_FLAGS, + DocumentsContract.Root.COLUMN_ICON, + DocumentsContract.Root.COLUMN_TITLE, + DocumentsContract.Root.COLUMN_SUMMARY, + DocumentsContract.Root.COLUMN_DOCUMENT_ID, + DocumentsContract.Root.COLUMN_AVAILABLE_BYTES + ) + private val DefaultDocumentProjection = arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_MIME_TYPE, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_LAST_MODIFIED, + DocumentsContract.Document.COLUMN_FLAGS, + DocumentsContract.Document.COLUMN_SIZE + ) } } 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 new file mode 100644 index 00000000..bf3250bf --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameDetailsScreen.kt @@ -0,0 +1,115 @@ +package info.cemu.cemu.gamelist + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import info.cemu.cemu.R +import info.cemu.cemu.guicore.ScreenContent +import info.cemu.cemu.guicore.enumtostringmapper.native.regionToStringId +import info.cemu.cemu.nativeinterface.NativeGameTitles.Game +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle + +@Composable +fun GameDetailsScreen(selectedGameViewModel: GameViewModel, navigateBack: () -> Unit) { + val game by selectedGameViewModel.game.collectAsState() + if (game == null) return + ScreenContent( + appBarText = stringResource(R.string.about_title), + contentModifier = Modifier.padding(16.dp), + contentVerticalArrangement = Arrangement.spacedBy(16.dp), + navigateBack = navigateBack, + ) { + GameDetails(game!!) + } +} + +@Composable +fun GameDetails(game: Game) { + GameIcon( + game = game, + modifier = Modifier.size(128.dp), + ) + TitleDetailsEntry(entryName = stringResource(R.string.title_name), entryData = game.name) + TitleDetailsEntry(entryName = stringResource(R.string.title_id), entryData = game.titleId) + TitleDetailsEntry(entryName = stringResource(R.string.version), entryData = game.version) + TitleDetailsEntry(entryName = stringResource(R.string.dlc), entryData = game.dlc) + TitleDetailsEntry( + entryName = stringResource(R.string.title_time_played), + entryData = getTimePlayed(game) + ) + TitleDetailsEntry( + entryName = stringResource(R.string.title_last_played), + entryData = getLastPlayedDate(game) + ) + TitleDetailsEntry( + entryName = stringResource(R.string.title_region), + entryData = stringResource(regionToStringId(game.region)) + ) + TitleDetailsEntry( + entryName = stringResource(R.string.title_path), + entryData = game.path + ) +} + + +@ReadOnlyComposable +@Composable +fun getTimePlayed(game: Game): String { + if (game.minutesPlayed == 0) { + return stringResource(R.string.never_played) + } + if (game.minutesPlayed < 60) { + return stringResource(R.string.minutes_played, game.minutesPlayed) + } + return stringResource( + R.string.hours_minutes_played, + game.minutesPlayed / 60, + game.minutesPlayed % 60 + ) +} + +private val DateFormatter = DateTimeFormatter.ofLocalizedDate( + FormatStyle.SHORT +) + +@ReadOnlyComposable +@Composable +private fun getLastPlayedDate(game: Game): String { + if (game.lastPlayedYear.toInt() == 0) { + return stringResource(R.string.never_played) + } + val lastPlayedDate = LocalDate.of( + game.lastPlayedYear.toInt(), + game.lastPlayedMonth.toInt(), + game.lastPlayedDay.toInt() + ) + return DateFormatter.format(lastPlayedDate) +} + +@Composable +fun TitleDetailsEntry(entryName: String, entryData: T?) { + Column { + Text( + text = entryName, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + ) + Text( + text = entryData?.toString() ?: "", + fontSize = 16.sp, + ) + } +} 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 new file mode 100644 index 00000000..8da4d6c8 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameIcon.kt @@ -0,0 +1,43 @@ +package info.cemu.cemu.gamelist + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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 +import info.cemu.cemu.nativeinterface.NativeGameTitles + + +@Composable +fun GameIcon( + game: NativeGameTitles.Game, + modifier: Modifier +) { + if (game.icon != null) { + Image( + modifier = modifier + .border( + width = Dp.Hairline, + color = MaterialTheme.colorScheme.onSurface, + shape = RoundedCornerShape(8.dp) + ) + .clip(RoundedCornerShape(8.dp)), + bitmap = game.icon!!, + contentDescription = stringResource(R.string.game_icon), + ) + } else { + Icon( + modifier = modifier, + painter = painterResource(R.drawable.ic_question_mark), + contentDescription = stringResource(R.string.game_icon_empty), + ) + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameListViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameListViewModel.kt new file mode 100644 index 00000000..198b4af7 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameListViewModel.kt @@ -0,0 +1,90 @@ +package info.cemu.cemu.gamelist + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import info.cemu.cemu.nativeinterface.NativeGameTitles +import info.cemu.cemu.nativeinterface.NativeGameTitles.Game +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +class GameListViewModel : ViewModel() { + private val _filterText = MutableStateFlow("") + private val filterText = _filterText.asStateFlow() + fun setFilterText(filterText: String) { + _filterText.value = filterText + } + + private val _games = MutableStateFlow>(emptySet()) + val games: StateFlow> = filterText.combine(_games) { filter, games -> + if (filter.isBlank()) { + games + } else { + games.filter { it.name?.contains(filter, true) ?: false } + } + }.map { + it.sorted() + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + emptyList() + ) + + fun getGames(): LiveData> = MutableLiveData(emptyList()) + + init { + NativeGameTitles.setGameTitleLoadedCallback(NativeGameTitles.GameTitleLoadedCallback { game: Game? -> + if (game == null || !isGameValid(game)) { + return@GameTitleLoadedCallback + } + _games.value += game + }) + refreshGames() + } + + private val _gameToRemoveShaders = MutableStateFlow(null) + val gameToRemoveShaders = _gameToRemoveShaders.asStateFlow() + fun removeShadersForSelectedGame() { + if (_gameToRemoveShaders.value == null) return + NativeGameTitles.removeShaderCacheFilesForTitle(_gameToRemoveShaders.value!!.titleId) + _gameToRemoveShaders.value = null + } + + fun setGameForShadersRemoval(game: Game) { + _gameToRemoveShaders.value = game + } + + fun clearSelectedGameForShaderRemoval() { + _gameToRemoveShaders.value = null + } + + private fun isGameValid(game: Game): Boolean { + return !game.path.isNullOrEmpty() && !game.name.isNullOrEmpty() + } + + fun setGameTitleFavorite(game: Game, isFavorite: Boolean) { + if (!_games.value.contains(game)) { + return + } + NativeGameTitles.setGameTitleFavorite(game.titleId, isFavorite) + _games.value = _games.value.toMutableSet().apply { + remove(game) + add(game.copy(isFavorite = isFavorite)) + } + } + + override fun onCleared() { + NativeGameTitles.setGameTitleLoadedCallback(null) + } + + fun refreshGames() { + _games.value = emptySet() + NativeGameTitles.reloadGameTitles() + } +} 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 new file mode 100644 index 00000000..fc57c66f --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameProfileEditScreen.kt @@ -0,0 +1,81 @@ +package info.cemu.cemu.gamelist + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import info.cemu.cemu.R +import info.cemu.cemu.guicore.Header +import info.cemu.cemu.guicore.ScreenContent +import info.cemu.cemu.guicore.SingleSelection +import info.cemu.cemu.guicore.Toggle +import info.cemu.cemu.guicore.enumtostringmapper.native.cpuModeToStringId +import info.cemu.cemu.nativeinterface.NativeGameTitles + +@Composable +fun GameProfileEditScreen(selectedGameViewModel: GameViewModel, navigateBack: () -> Unit) { + val game = selectedGameViewModel.game.collectAsStateWithLifecycle().value ?: return + val titleId = game.titleId + ScreenContent( + appBarText = stringResource(R.string.edit_game_profile), + navigateBack = navigateBack, + contentModifier = Modifier.padding(16.dp), + contentVerticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Header(text = game.name) + Toggle( + label = stringResource(R.string.load_shared_libraries), + description = stringResource(R.string.load_shared_libraries_description), + initialCheckedState = { + NativeGameTitles.isLoadingSharedLibrariesForTitleEnabled( + titleId + ) + }, + onCheckedChanged = { enabled -> + NativeGameTitles.setLoadingSharedLibrariesForTitleEnabled( + titleId, + enabled + ) + }, + ) + Toggle( + label = stringResource(R.string.shader_multiplication_accuracy), + description = stringResource(R.string.shader_multiplication_accuracy_description), + initialCheckedState = { + NativeGameTitles.isShaderMultiplicationAccuracyForTitleEnabled( + titleId + ) + }, + onCheckedChanged = { enabled -> + NativeGameTitles.setShaderMultiplicationAccuracyForTitleEnabled( + titleId, + enabled + ) + }, + ) + SingleSelection( + label = stringResource(R.string.cpu_mode), + initialChoice = { NativeGameTitles.getCpuModeForTitle(titleId) }, + choices = listOf( + NativeGameTitles.CPU_MODE_SINGLECOREINTERPRETER, + NativeGameTitles.CPU_MODE_SINGLECORERECOMPILER, + NativeGameTitles.CPU_MODE_MULTICORERECOMPILER, + NativeGameTitles.CPU_MODE_AUTO + ), + choiceToString = { cpuMode -> stringResource(cpuModeToStringId(cpuMode)) }, + onChoiceChanged = { cpuMode -> NativeGameTitles.setCpuModeForTitle(titleId, cpuMode) } + ) + SingleSelection( + label = stringResource(R.string.thread_quantum), + initialChoice = { NativeGameTitles.getThreadQuantumForTitle(titleId) }, + choices = NativeGameTitles.THREAD_QUANTUM_VALUES.toList(), + choiceToString = { it.toString() }, + onChoiceChanged = { threadQuantum -> + NativeGameTitles.setThreadQuantumForTitle(titleId, threadQuantum) + } + ) + } +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameViewModel.kt new file mode 100644 index 00000000..625a5810 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameViewModel.kt @@ -0,0 +1,16 @@ +package info.cemu.cemu.gamelist + +import androidx.lifecycle.ViewModel +import info.cemu.cemu.nativeinterface.NativeGameTitles.Game +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class GameViewModel : ViewModel() { + private val _game = MutableStateFlow(null) + val game: StateFlow + get() = _game + + fun setCurrentGame(game: Game?) { + _game.value = game + } +} 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 new file mode 100644 index 00000000..be0e88c5 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GamesListScreen.kt @@ -0,0 +1,396 @@ +@file:OptIn( + ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class, +) + +package info.cemu.cemu.gamelist + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material3.pulltorefresh.pullToRefresh +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +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 +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +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.emulation.EmulationActivity +import info.cemu.cemu.guicore.FilledSearchToolbar +import info.cemu.cemu.nativeinterface.NativeGameTitles +import info.cemu.cemu.nativeinterface.NativeGameTitles.Game +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import android.graphics.drawable.Icon as ShortcutIcon + +data class GamesListScreenActions( + val goToGameDetails: () -> Unit, + val goToGameEditProfile: () -> Unit, + val startGame: (Game) -> Unit, +) + + +@Composable +fun GamesListScreen( + selectedGameViewModel: GameViewModel, + gameListViewModel: GameListViewModel = viewModel(), + gameListActions: GamesListScreenActions, + toolbarActions: @Composable RowScope.() -> Unit, +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + var refreshing by remember { mutableStateOf(false) } + val gameToRemoveShaders by gameListViewModel.gameToRemoveShaders.collectAsStateWithLifecycle() + val snackbarHostState = remember { SnackbarHostState() } + + fun onRefresh() = coroutineScope.launch { + refreshing = true + gameListViewModel.refreshGames() + delay(1500) + refreshing = false + } + + val state = rememberPullToRefreshState() + + DisposableEffect(LocalLifecycleOwner.current) { + onDispose { + gameListViewModel.setFilterText("") + } + } + Scaffold( + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + topBar = { + FilledSearchToolbar( + actions = toolbarActions, + hint = stringResource(R.string.search_games), + onValueChange = gameListViewModel::setFilterText + ) + }, + ) { scaffoldPadding -> + Box( + modifier = Modifier + .padding(scaffoldPadding) + .fillMaxSize() + .pullToRefresh( + isRefreshing = refreshing, + state = state, + onRefresh = ::onRefresh, + ), + ) { + GameList( + gameListViewModel = gameListViewModel, + selectedGameViewModel = selectedGameViewModel, + actions = gameListActions, + onFailedToCreateShortCut = { + coroutineScope.launch { snackbarHostState.showSnackbar(context.getString(R.string.shortcut_not_supported)) } + } + ) + PullToRefreshDefaults.Indicator( + modifier = Modifier.align(Alignment.TopCenter), + isRefreshing = refreshing, + state = state, + containerColor = MaterialTheme.colorScheme.surfaceVariant, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + if (gameToRemoveShaders != null) { + ShaderCachesConfirmationDialog( + gameName = gameToRemoveShaders?.name ?: "", + onDismissRequest = gameListViewModel::clearSelectedGameForShaderRemoval, + onConfirm = { + gameListViewModel.removeShadersForSelectedGame() + coroutineScope.launch { snackbarHostState.showSnackbar(context.getString(R.string.shader_caches_removed_notification)) } + }, + ) + } +} + + +@Composable +fun GameList( + gameListViewModel: GameListViewModel, + selectedGameViewModel: GameViewModel, + actions: GamesListScreenActions, + onFailedToCreateShortCut: () -> Unit, +) { + val games by gameListViewModel.games.collectAsStateWithLifecycle() + val context = LocalContext.current + LazyVerticalGrid( + modifier = Modifier + .padding(8.dp) + .fillMaxSize(), + columns = GridCells.Adaptive(620.dp) + ) { + items(items = games, key = { it.titleId }) { game -> + GameListItem( + modifier = Modifier.animateItem(), + game = game, + onStartGame = actions.startGame, + actions = GameContextMenuActions( + onIsFavoriteChanged = { isFavorite -> + gameListViewModel.setGameTitleFavorite(game, isFavorite) + }, + onEditGameProfile = { + selectedGameViewModel.setCurrentGame(game) + actions.goToGameEditProfile() + }, + onRemoveShaderCaches = { gameListViewModel.setGameForShadersRemoval(game) }, + onAboutTitle = { + selectedGameViewModel.setCurrentGame(game) + actions.goToGameDetails() + }, + onCreateShortcut = { + createShortcutForGame( + context = context, + game = game, + onFailedToCreateShortCut = onFailedToCreateShortCut + ) + }, + ), + ) + } + } +} + +@Composable +fun ShaderCachesConfirmationDialog( + gameName: String, + onDismissRequest: () -> Unit, + onConfirm: () -> Unit, +) { + AlertDialog( + title = { + Text(stringResource(R.string.remove_shader_caches)) + }, + text = { + Text(stringResource(R.string.remove_shader_caches_message, gameName)) + }, + dismissButton = { + TextButton( + onClick = onDismissRequest + ) { + Text(stringResource(R.string.no)) + } + }, + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton( + onClick = onConfirm + ) { + Text(stringResource(R.string.yes)) + } + } + ) +} + +@Composable +fun GameListItem( + actions: GameContextMenuActions, + onStartGame: (Game) -> Unit, + game: Game, + modifier: Modifier = Modifier, +) { + var contextMenuExpanded by rememberSaveable { mutableStateOf(false) } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .combinedClickable( + onClick = { onStartGame(game) }, + onLongClick = { + contextMenuExpanded = true + }, + ) + .padding(8.dp) + .fillMaxWidth(), + ) { + Box { + GameIcon( + game = game, + modifier = Modifier.size(60.dp), + ) + if (game.isFavorite) { + Icon( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(2.dp) + .size(24.dp) + .clip(androidx.compose.foundation.shape.CircleShape) + .background(MaterialTheme.colorScheme.surfaceVariant), + painter = painterResource(R.drawable.ic_favorite), + tint = MaterialTheme.colorScheme.primary, + contentDescription = stringResource(R.string.game_favorite_description), + ) + } + } + Text( + modifier = Modifier.padding(horizontal = 8.dp), + text = game.name ?: "", + fontSize = 24.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + GameContextMenu( + expanded = contextMenuExpanded, + onDismissRequest = { contextMenuExpanded = false }, + actions = actions, + game = game, + ) + } +} + +data class GameContextMenuActions( + val onIsFavoriteChanged: (Boolean) -> Unit, + val onEditGameProfile: () -> Unit, + val onRemoveShaderCaches: () -> Unit, + val onAboutTitle: () -> Unit, + val onCreateShortcut: () -> Unit, +) + +@Composable +fun GameContextMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + actions: GameContextMenuActions, + game: Game, +) { + @Composable + fun GameContextMenuItem( + onClick: () -> Unit, + text: String, + enabled: Boolean = true, + trailingIcon: @Composable (() -> Unit)? = null, + ) { + DropdownMenuItem( + enabled = enabled, + onClick = { + onDismissRequest() + onClick() + }, + text = { + Text(text = text) + }, + trailingIcon = trailingIcon, + ) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = onDismissRequest + ) { + val gameTitleHasCaches = rememberSaveable { + NativeGameTitles.titleHasShaderCacheFiles(game.titleId) + } + GameContextMenuItem( + onClick = { actions.onIsFavoriteChanged(!game.isFavorite) }, + text = stringResource(R.string.game_favorite), + trailingIcon = { + Checkbox(checked = game.isFavorite, onCheckedChange = null) + } + ) + GameContextMenuItem( + onClick = actions.onEditGameProfile, + text = stringResource(R.string.edit_game_profile) + ) + GameContextMenuItem( + enabled = gameTitleHasCaches, + onClick = { + actions.onRemoveShaderCaches() + }, + text = stringResource(R.string.remove_shader_caches) + ) + GameContextMenuItem( + onClick = actions.onAboutTitle, + text = stringResource(R.string.about_title), + ) + GameContextMenuItem( + onClick = actions.onCreateShortcut, + text = stringResource(R.string.create_shortcut) + ) + } +} + + +private fun createShortcutForGame( + context: Context, + game: Game, + onFailedToCreateShortCut: () -> Unit, +) { + val shortcutManager = context.getSystemService( + ShortcutManager::class.java + ) + if (!shortcutManager.isRequestPinShortcutSupported) { + onFailedToCreateShortCut() + return + } + val icon = game.icon?.asAndroidBitmap().let { + if (it != null) ShortcutIcon.createWithBitmap(it) + else ShortcutIcon.createWithResource(context, R.mipmap.ic_launcher) + } + val intent = Intent( + context, + EmulationActivity::class.java + ) + intent.setAction(Intent.ACTION_VIEW) + intent.putExtra(EmulationActivity.EXTRA_LAUNCH_PATH, game.path) + val pinShortcutInfo = ShortcutInfo.Builder(context, game.titleId.toString()) + .setShortLabel(game.name!!) + .setIntent(intent) + .setIcon(icon) + .build() + val pinnedShortcutCallbackIntent = + shortcutManager.createShortcutResultIntent(pinShortcutInfo) + val successCallback = PendingIntent.getBroadcast( + context, + 0, + pinnedShortcutCallbackIntent, + PendingIntent.FLAG_IMMUTABLE + ) + shortcutManager.requestPinShortcut(pinShortcutInfo, successCallback.intentSender) +} + + diff --git a/src/android/app/src/main/java/info/cemu/cemu/gameview/GameAdapter.kt b/src/android/app/src/main/java/info/cemu/cemu/gameview/GameAdapter.kt deleted file mode 100644 index 3ed6accf..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/gameview/GameAdapter.kt +++ /dev/null @@ -1,88 +0,0 @@ -package info.cemu.cemu.gameview - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.card.MaterialCardView -import info.cemu.cemu.R -import info.cemu.cemu.nativeinterface.NativeGameTitles.Game - -class GameAdapter(private val gameTitleClickAction: GameTitleClickAction) : - ListAdapter(DIFF_CALLBACK) { - private var originalGameList: List? = null - private var filterText: String = "" - var selectedGame: Game? = null - private set - - fun interface GameTitleClickAction { - fun action(game: Game) - } - - override fun submitList(list: List?) { - originalGameList = list ?: emptyList() - updateGameList() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.layout_game, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val game = getItem(position) ?: return - holder.icon.setImageBitmap(game.icon) - holder.favoriteIcon.visibility = if (game.isFavorite) View.VISIBLE else View.GONE - holder.text.text = game.name - holder.itemView.setOnClickListener { _: View? -> - gameTitleClickAction.action( - game - ) - } - holder.itemView.setOnLongClickListener { _: View? -> - selectedGame = game - false - } - } - - fun setFilterText(filterText: String?) { - this.filterText = filterText?.lowercase() ?: "" - updateGameList() - } - - private fun updateGameList() { - if (filterText.isBlank() || originalGameList == null) { - super.submitList(originalGameList) - return - } - super.submitList(originalGameList!!.filter { - it.name?.lowercase()?.contains(filterText) ?: false - }) - } - - class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var icon: ImageView = - itemView.findViewById(R.id.game_icon) - var text: TextView = - itemView.findViewById(R.id.game_title) - var favoriteIcon: MaterialCardView = - itemView.findViewById(R.id.game_favorite_icon) - } - - companion object { - @JvmField - val DIFF_CALLBACK: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean { - return oldItem.titleId == newItem.titleId - } - - override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean { - return oldItem.path == newItem.path && oldItem.titleId == newItem.titleId && oldItem.isFavorite == newItem.isFavorite - } - } - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/gameview/GameDetailsFragment.kt b/src/android/app/src/main/java/info/cemu/cemu/gameview/GameDetailsFragment.kt deleted file mode 100644 index 2b7998cd..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/gameview/GameDetailsFragment.kt +++ /dev/null @@ -1,95 +0,0 @@ -package info.cemu.cemu.gameview - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.StringRes -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.fragment.NavHostFragment -import androidx.navigation.ui.AppBarConfiguration.Builder -import androidx.navigation.ui.NavigationUI.setupWithNavController -import info.cemu.cemu.R -import info.cemu.cemu.databinding.FragmentGameDetailsBinding -import info.cemu.cemu.nativeinterface.NativeGameTitles -import info.cemu.cemu.nativeinterface.NativeGameTitles.Game -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle - -class GameDetailsFragment : Fragment() { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val binding = FragmentGameDetailsBinding.inflate(inflater, container, false) - val game = ViewModelProvider(requireActivity()).get(GameViewModel::class.java).game - binding.gameTitleName.text = game!!.name - binding.titleVersion.text = game.version.toString() - if (game.icon != null) { - binding.titleIcon.setImageBitmap(game.icon) - } - if (game.dlc.toInt() != 0) { - binding.titleDlc.text = game.dlc.toString() - } - binding.titleTimePlayed.text = getTimePlayed(game) - binding.titleLastPlayed.text = getLastPlayedDate(game) - binding.titleId.text = String.format("%016x", game.titleId) - binding.titleRegion.setText(getRegionName(game)) - setupWithNavController( - binding.gameDetailsToolbar, - NavHostFragment.findNavController(this), - Builder().build() - ) - return binding.root - } - - private fun getLastPlayedDate(game: Game): String { - if (game.lastPlayedYear.toInt() == 0) { - return getString(R.string.never_played) - } - val lastPlayedDate = LocalDate.of( - game.lastPlayedYear.toInt(), - game.lastPlayedMonth.toInt(), - game.lastPlayedDay.toInt() - ) - return DATE_FORMATTER.format(lastPlayedDate) - } - - private fun getTimePlayed(game: Game): String { - if (game.minutesPlayed == 0) { - return getString(R.string.never_played) - } - if (game.minutesPlayed < 60) { - return getString(R.string.minutes_played, game.minutesPlayed) - } - return getString( - R.string.hours_minutes_played, - game.minutesPlayed / 60, - game.minutesPlayed % 60 - ) - } - - @StringRes - private fun getRegionName(game: Game): Int { - return when (game.region) { - NativeGameTitles.CONSOLE_REGION_JPN -> R.string.console_region_japan - NativeGameTitles.CONSOLE_REGION_USA -> R.string.console_region_usa - NativeGameTitles.CONSOLE_REGION_EUR -> R.string.console_region_europe - NativeGameTitles.CONSOLE_REGION_AUS_DEPR -> R.string.console_region_australia - NativeGameTitles.CONSOLE_REGION_CHN -> R.string.console_region_china - NativeGameTitles.CONSOLE_REGION_KOR -> R.string.console_region_korea - NativeGameTitles.CONSOLE_REGION_TWN -> R.string.console_region_taiwan - NativeGameTitles.CONSOLE_REGION_AUTO -> R.string.console_region_auto - else -> R.string.console_region_many - } - } - - companion object { - private val DATE_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate( - FormatStyle.SHORT - ) - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/gameview/GameListViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/gameview/GameListViewModel.kt deleted file mode 100644 index 5ddec5cf..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/gameview/GameListViewModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -package info.cemu.cemu.gameview - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import info.cemu.cemu.nativeinterface.NativeGameTitles.Game -import info.cemu.cemu.nativeinterface.NativeGameTitles.GameTitleLoadedCallback -import info.cemu.cemu.nativeinterface.NativeGameTitles.reloadGameTitles -import info.cemu.cemu.nativeinterface.NativeGameTitles.setGameTitleFavorite -import info.cemu.cemu.nativeinterface.NativeGameTitles.setGameTitleLoadedCallback -import java.util.TreeSet - -class GameListViewModel : ViewModel() { - private val gamesData = - MutableLiveData?>() - - private val games = TreeSet() - - fun getGames(): LiveData?> { - return gamesData - } - - init { - setGameTitleLoadedCallback(GameTitleLoadedCallback { game: Game? -> - if (game == null || !isGameValid(game)) { - return@GameTitleLoadedCallback - } - synchronized(this@GameListViewModel) { - games.add(game) - gamesData.postValue(ArrayList(games)) - } - }) - } - - private fun isGameValid(game: Game): Boolean { - return game.path != null && game.name != null - } - - fun setGameTitleFavorite(game: Game, isFavorite: Boolean) { - synchronized(this) { - if (!games.contains(game)) { - return - } - setGameTitleFavorite(game.titleId, isFavorite) - games.remove(game) - games.add(game.copy(isFavorite = isFavorite)) - gamesData.postValue(ArrayList(games)) - } - } - - override fun onCleared() { - setGameTitleLoadedCallback(null) - } - - fun refreshGames() { - games.clear() - gamesData.value = null - reloadGameTitles() - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/gameview/GameProfileEditFragment.kt b/src/android/app/src/main/java/info/cemu/cemu/gameview/GameProfileEditFragment.kt deleted file mode 100644 index c70238ef..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/gameview/GameProfileEditFragment.kt +++ /dev/null @@ -1,117 +0,0 @@ -package info.cemu.cemu.gameview - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.fragment.NavHostFragment -import androidx.navigation.ui.AppBarConfiguration.Builder -import androidx.navigation.ui.NavigationUI.setupWithNavController -import info.cemu.cemu.R -import info.cemu.cemu.databinding.FragmentGameProfileEditBinding -import info.cemu.cemu.guibasecomponents.GenericRecyclerViewAdapter -import info.cemu.cemu.guibasecomponents.HeaderRecyclerViewItem -import info.cemu.cemu.guibasecomponents.SingleSelectionRecyclerViewItem -import info.cemu.cemu.guibasecomponents.ToggleRecyclerViewItem -import info.cemu.cemu.nativeinterface.NativeGameTitles -import info.cemu.cemu.nativeinterface.NativeGameTitles.getCpuModeForTitle -import info.cemu.cemu.nativeinterface.NativeGameTitles.getThreadQuantumForTitle -import info.cemu.cemu.nativeinterface.NativeGameTitles.isLoadingSharedLibrariesForTitleEnabled -import info.cemu.cemu.nativeinterface.NativeGameTitles.isShaderMultiplicationAccuracyForTitleEnabled -import info.cemu.cemu.nativeinterface.NativeGameTitles.setCpuModeForTitle -import info.cemu.cemu.nativeinterface.NativeGameTitles.setLoadingSharedLibrariesForTitleEnabled -import info.cemu.cemu.nativeinterface.NativeGameTitles.setShaderMultiplicationAccuracyForTitleEnabled -import info.cemu.cemu.nativeinterface.NativeGameTitles.setThreadQuantumForTitle -import java.util.Arrays -import java.util.stream.Collectors - -class GameProfileEditFragment : Fragment() { - private fun cpuModeToString(cpuMode: Int): String { - val resourceId = when (cpuMode) { - NativeGameTitles.CPU_MODE_SINGLECOREINTERPRETER -> R.string.cpu_mode_single_core_interpreter - NativeGameTitles.CPU_MODE_SINGLECORERECOMPILER -> R.string.cpu_mode_single_core_recompiler - NativeGameTitles.CPU_MODE_MULTICORERECOMPILER -> R.string.cpu_mode_multi_core_recompiler - else -> R.string.cpu_mode_auto - } - return getString(resourceId) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val binding = FragmentGameProfileEditBinding.inflate(inflater, container, false) - val game = ViewModelProvider(requireActivity()).get(GameViewModel::class.java).game - val titleId = game!!.titleId - - val genericRecyclerViewAdapter = GenericRecyclerViewAdapter() - - genericRecyclerViewAdapter.addRecyclerViewItem(HeaderRecyclerViewItem(game.name)) - - val loadSharedLibrariesToggle = ToggleRecyclerViewItem( - "Load shared libraries", - "Load libraries from the cafeLibs directory", - isLoadingSharedLibrariesForTitleEnabled(titleId) - ) { checked: Boolean -> - setLoadingSharedLibrariesForTitleEnabled( - titleId, - checked - ) - } - genericRecyclerViewAdapter.addRecyclerViewItem(loadSharedLibrariesToggle) - - val shaderMultiplicationAccuracyToggle = ToggleRecyclerViewItem( - "Shader multiplication accuracy", - "Controls the accuracy of floating point multiplication in shaders", - isShaderMultiplicationAccuracyForTitleEnabled(titleId) - ) { checked: Boolean -> - setShaderMultiplicationAccuracyForTitleEnabled( - titleId, - checked - ) - } - genericRecyclerViewAdapter.addRecyclerViewItem(shaderMultiplicationAccuracyToggle) - - val cpuModeSelection = SingleSelectionRecyclerViewItem(getString(R.string.cpu_mode), - getCpuModeForTitle(titleId), - listOf( - NativeGameTitles.CPU_MODE_SINGLECOREINTERPRETER, - NativeGameTitles.CPU_MODE_SINGLECORERECOMPILER, - NativeGameTitles.CPU_MODE_MULTICORERECOMPILER, - NativeGameTitles.CPU_MODE_AUTO - ), - { cpuMode: Int -> this.cpuModeToString(cpuMode) }, - { cpuMode: Int? -> - setCpuModeForTitle( - titleId, - cpuMode!! - ) - }) - genericRecyclerViewAdapter.addRecyclerViewItem(cpuModeSelection) - - val threadQuantumSelection = - SingleSelectionRecyclerViewItem(getString(R.string.thread_quantum), - getThreadQuantumForTitle(titleId), - Arrays.stream(NativeGameTitles.THREAD_QUANTUM_VALUES).boxed() - .collect(Collectors.toList()), - { obj: Int? -> java.lang.String.valueOf(obj) }, - { threadQuantum: Int? -> - setThreadQuantumForTitle( - titleId, - threadQuantum!! - ) - }) - genericRecyclerViewAdapter.addRecyclerViewItem(threadQuantumSelection) - - binding.recyclerView.adapter = genericRecyclerViewAdapter - setupWithNavController( - binding.gameEditProfileToolbar, NavHostFragment.findNavController( - this - ), Builder().build() - ) - return binding.root - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/gameview/GameViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/gameview/GameViewModel.kt deleted file mode 100644 index e595e442..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/gameview/GameViewModel.kt +++ /dev/null @@ -1,9 +0,0 @@ -package info.cemu.cemu.gameview - -import androidx.lifecycle.ViewModel -import info.cemu.cemu.nativeinterface.NativeGameTitles.Game - -class GameViewModel : ViewModel() { - @JvmField - var game: Game? = null -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/gameview/GamesFragment.kt b/src/android/app/src/main/java/info/cemu/cemu/gameview/GamesFragment.kt deleted file mode 100644 index 55fc129b..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/gameview/GamesFragment.kt +++ /dev/null @@ -1,231 +0,0 @@ -package info.cemu.cemu.gameview - -import android.app.PendingIntent -import android.content.DialogInterface -import android.content.Intent -import android.content.pm.ShortcutInfo -import android.content.pm.ShortcutManager -import android.graphics.Color -import android.graphics.drawable.Icon -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.view.ContextMenu -import android.view.ContextMenu.ContextMenuInfo -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.appcompat.widget.SearchView -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.fragment.NavHostFragment -import com.google.android.material.color.MaterialColors -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import info.cemu.cemu.R -import info.cemu.cemu.databinding.FragmentGamesBinding -import info.cemu.cemu.emulation.EmulationActivity -import info.cemu.cemu.gameview.GameAdapter.GameTitleClickAction -import info.cemu.cemu.nativeinterface.NativeGameTitles.Game -import info.cemu.cemu.nativeinterface.NativeGameTitles.reloadGameTitles -import info.cemu.cemu.nativeinterface.NativeGameTitles.removeShaderCacheFilesForTitle -import info.cemu.cemu.nativeinterface.NativeGameTitles.titleHasShaderCacheFiles -import info.cemu.cemu.nativeinterface.NativeSettings.gamesPaths -import info.cemu.cemu.settings.SettingsActivity -import java.util.Objects - -class GamesFragment : Fragment() { - private var gameAdapter: GameAdapter? = null - private var gameListViewModel: GameListViewModel? = null - private var gameViewModel: GameViewModel? = null - private var refreshing = false - private val handler = Handler(Looper.getMainLooper()) - private var currentGamePaths = HashSet() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - currentGamePaths = HashSet(gamesPaths) - gameAdapter = GameAdapter { game: Game -> - val intent = Intent( - requireContext(), - EmulationActivity::class.java - ) - intent.putExtra(EmulationActivity.EXTRA_LAUNCH_PATH, game.path) - startActivity(intent) - } - gameListViewModel = ViewModelProvider(this).get( - GameListViewModel::class.java - ) - gameViewModel = ViewModelProvider(requireActivity()).get(GameViewModel::class.java) - gameListViewModel!!.getGames() - .observe(this) { gameList: List? -> gameAdapter!!.submitList(gameList) } - reloadGameTitles() - } - - override fun onResume() { - super.onResume() - val gamePaths = HashSet(gamesPaths) - if (currentGamePaths != gamePaths) { - currentGamePaths = gamePaths - reloadGameTitles() - } - gameAdapter!!.setFilterText(null) - } - - override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenuInfo?) { - super.onCreateContextMenu(menu, v, menuInfo) - val inflater = requireActivity().menuInflater - inflater.inflate(R.menu.game, menu) - val selectedGame = gameAdapter!!.selectedGame ?: return - menu.findItem(R.id.favorite).setChecked(selectedGame.isFavorite) - menu.findItem(R.id.remove_shader_caches) - .setEnabled(titleHasShaderCacheFiles(selectedGame.titleId)) - } - - override fun onContextItemSelected(item: MenuItem): Boolean { - val game = gameAdapter!!.selectedGame ?: return super.onContextItemSelected(item) - val itemId = item.itemId - if (itemId == R.id.favorite) { - gameListViewModel!!.setGameTitleFavorite(game, !game.isFavorite) - return true - } - if (itemId == R.id.game_profile) { - gameViewModel!!.game = game - NavHostFragment.findNavController(this) - .navigate(R.id.action_games_fragment_to_game_edit_profile) - return true - } - if (itemId == R.id.remove_shader_caches) { - removeShaderCachesForGame(game) - return true - } - if (itemId == R.id.about_title) { - gameViewModel!!.game = game - NavHostFragment.findNavController(this) - .navigate(R.id.action_games_fragment_to_game_details_fragment) - return true - } - if (itemId == R.id.create_shortcut) { - createShortcutForGame(game) - return true - } - return super.onContextItemSelected(item) - } - - private fun createShortcutForGame(game: Game) { - val context = requireContext() - val shortcutManager = context.getSystemService( - ShortcutManager::class.java - ) - if (!shortcutManager.isRequestPinShortcutSupported) { - Toast.makeText(context, R.string.shortcut_not_supported, Toast.LENGTH_SHORT).show() - return - } - if (game.icon == null) { - return - } - val intent = Intent( - requireContext(), - EmulationActivity::class.java - ) - intent.setAction(Intent.ACTION_VIEW) - intent.putExtra(EmulationActivity.EXTRA_LAUNCH_PATH, game.path) - val pinShortcutInfo = ShortcutInfo.Builder(context, game.titleId.toString()) - .setShortLabel(game.name!!) - .setIntent(intent) - .setIcon(Icon.createWithBitmap(game.icon)) - .build() - val pinnedShortcutCallbackIntent = - shortcutManager.createShortcutResultIntent(pinShortcutInfo) - val successCallback = PendingIntent.getBroadcast( - context, - 0, - pinnedShortcutCallbackIntent, - PendingIntent.FLAG_IMMUTABLE - ) - shortcutManager.requestPinShortcut(pinShortcutInfo, successCallback.intentSender) - } - - private fun removeShaderCachesForGame(game: Game) { - val builder = MaterialAlertDialogBuilder(requireContext()) - builder.setTitle(R.string.remove_shader_caches) - .setMessage(getString(R.string.remove_shader_caches_message, game.name)) - .setPositiveButton(R.string.yes) { dialog: DialogInterface?, which: Int -> - removeShaderCacheFilesForTitle(game.titleId) - Toast.makeText( - requireContext(), - R.string.shader_caches_removed_notification, - Toast.LENGTH_SHORT - ).show() - } - .setNegativeButton( - R.string.no - ) { dialog: DialogInterface, which: Int -> dialog.dismiss() } - .show() - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val binding = FragmentGamesBinding.inflate(inflater, container, false) - val recyclerView = binding.gamesRecyclerView - registerForContextMenu(recyclerView) - binding.settingsButton.setOnClickListener { v: View? -> - val intent = Intent( - requireActivity(), - SettingsActivity::class.java - ) - startActivity(intent) - } - binding.searchBar.inflateMenu(R.menu.game_list) - val searchMenuItem = binding.searchBar.menu.findItem(R.id.action_search) - binding.searchBar.setOnClickListener { v: View? -> searchMenuItem.expandActionView() } - val searchView = Objects.requireNonNull(searchMenuItem.actionView) as SearchView - val searchPlate = searchView.findViewById(androidx.appcompat.R.id.search_plate) - searchPlate?.setBackgroundColor(Color.TRANSPARENT) - searchView.queryHint = getString(R.string.search_games) - searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String): Boolean { - gameAdapter!!.setFilterText(query) - return true - } - - override fun onQueryTextChange(newText: String): Boolean { - gameAdapter!!.setFilterText(newText) - return true - } - }) - - binding.gamesSwipeRefresh.setOnRefreshListener { - if (refreshing) { - return@setOnRefreshListener - } - refreshing = true - handler.postDelayed({ - binding.gamesSwipeRefresh.isRefreshing = false - refreshing = false - }, 1000) - gameListViewModel!!.refreshGames() - } - binding.gamesSwipeRefresh.setColorSchemeColors( - MaterialColors.getColor( - requireContext(), - com.google.android.material.R.attr.colorOnSurfaceVariant, - Color.BLACK - ) - ) - binding.gamesSwipeRefresh.setProgressBackgroundColorSchemeColor( - MaterialColors.getColor( - requireContext(), - com.google.android.material.R.attr.colorSurfaceVariant, - Color.WHITE - ) - ) - recyclerView.adapter = gameAdapter - - return binding.root - } -} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackDataNode.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackDataNode.kt similarity index 71% rename from src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackDataNode.kt rename to src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackDataNode.kt index 9c9eaea5..a1640ca6 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackDataNode.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackDataNode.kt @@ -1,11 +1,11 @@ -package info.cemu.cemu.settings.graphicpacks +package info.cemu.cemu.graphicpacks class GraphicPackDataNode( - @JvmField val id: Long, + val id: Long, name: String, val path: String, enabled: Boolean, - private val parentNode: GraphicPackSectionNode? + val parent: GraphicPackSectionNode?, ) : GraphicPackNode(name) { constructor( @@ -14,7 +14,7 @@ class GraphicPackDataNode( path: String, enabled: Boolean, titleIdInstalled: Boolean, - parentNode: GraphicPackSectionNode + parentNode: GraphicPackSectionNode, ) : this(id, name, path, enabled, parentNode) { this.titleIdInstalled = titleIdInstalled } @@ -25,6 +25,6 @@ class GraphicPackDataNode( return } field = value - parentNode?.updateEnabledCount(value) + parent?.updateEnabledCount(value) } } \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackDataViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackDataViewModel.kt new file mode 100644 index 00000000..3fd34b39 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackDataViewModel.kt @@ -0,0 +1,78 @@ +package info.cemu.cemu.graphicpacks + +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import info.cemu.cemu.nativeinterface.NativeGraphicPacks +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +data class Preset( + val index: Int, + val category: String?, + val activePreset: String, + val presets: List, +) + +class GraphicPackDataViewModel(private val graphicPackNode: GraphicPackDataNode) : ViewModel() { + private val nativeGraphicPack = NativeGraphicPacks.getGraphicPack(graphicPackNode.id) + val description = nativeGraphicPack?.description ?: "" + private var nativePresets: List = emptyList() + private val _presets = MutableStateFlow>(emptyList()) + val presets = _presets.asStateFlow() + + val name = graphicPackNode.name + + private var _enabled = MutableStateFlow(graphicPackNode.enabled) + val enabled = _enabled.asStateFlow() + fun setEnabled(enabled: Boolean) { + nativeGraphicPack?.setActive(enabled) + _enabled.value = enabled + graphicPackNode.enabled = enabled + } + + init { + refreshPresets() + } + + private fun refreshPresets() { + nativeGraphicPack?.reloadPresets() + if (nativePresets == nativeGraphicPack?.presets) { + return + } + nativePresets = nativeGraphicPack?.presets ?: emptyList() + nativePresets.mapIndexed { index, preset -> + Preset( + index = index, + category = preset.category, + activePreset = preset.activePreset, + presets = preset.presets, + ) + }.let { + _presets.value = it + } + } + + fun setActivePreset(index: Int, activePreset: String) { + _presets.value = _presets.value.toMutableList().apply { + set(index, get(index).copy(activePreset = activePreset)) + } + nativePresets[index].activePreset = activePreset + refreshPresets() + } + + companion object { + val GRAPHIC_PACK_KEY = object : CreationExtras.Key {} + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + GraphicPackDataViewModel( + this[GRAPHIC_PACK_KEY] as GraphicPackDataNode + ) + } + } + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackNode.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackNode.kt similarity index 60% rename from src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackNode.kt rename to src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackNode.kt index 97ebbd01..1e550acc 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackNode.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackNode.kt @@ -1,7 +1,7 @@ -package info.cemu.cemu.settings.graphicpacks +package info.cemu.cemu.graphicpacks abstract class GraphicPackNode( - var name: String? + val name: String? ) { var titleIdInstalled: Boolean = false protected set diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackSectionNode.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackSectionNode.kt similarity index 96% rename from src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackSectionNode.kt rename to src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackSectionNode.kt index 3f0edae0..f6fc6bb2 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackSectionNode.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackSectionNode.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.settings.graphicpacks +package info.cemu.cemu.graphicpacks import info.cemu.cemu.nativeinterface.NativeGraphicPacks import kotlin.math.max @@ -17,7 +17,7 @@ class GraphicPackSectionNode : GraphicPackNode { this.parent = parent } - private val parent: GraphicPackSectionNode? + val parent: GraphicPackSectionNode? var enabledGraphicPacksCount: Int = 0 private set var children: ArrayList = ArrayList() diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackSectionViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackSectionViewModel.kt new file mode 100644 index 00000000..1bfb30aa --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackSectionViewModel.kt @@ -0,0 +1,2 @@ +package info.cemu.cemu.graphicpacks + diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackViewModel.kt similarity index 58% rename from src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackViewModel.kt rename to src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackViewModel.kt index 25d78e11..c95ba767 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackViewModel.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackViewModel.kt @@ -1,6 +1,5 @@ -package info.cemu.cemu.settings.graphicpacks +package info.cemu.cemu.graphicpacks import androidx.lifecycle.ViewModel -class GraphicPackViewModel(var graphicPackNode: GraphicPackNode? = null) : ViewModel() { -} \ No newline at end of file +class GraphicPackViewModel(var graphicPackNode: GraphicPackNode? = null) : ViewModel() \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksActivity.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksActivity.kt new file mode 100644 index 00000000..0c2f0396 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksActivity.kt @@ -0,0 +1,119 @@ +package info.cemu.cemu.graphicpacks + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.AnimatedContentScope +import androidx.compose.runtime.Composable +import androidx.lifecycle.viewmodel.MutableCreationExtras +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import info.cemu.cemu.guicore.ActivityContent +import kotlinx.serialization.Serializable + +class GraphicPacksActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + ActivityContent { + GraphicPacksNav(parentNavBack = ::onNavigateUp) + } + } + } +} + +object GraphicPackRoutes { + @Serializable + object GraphicPacksRootSectionRoute + + @Serializable + object GraphicPackSectionScreenRoute + + @Serializable + object GraphicPackDataScreenRoute +} + +private inline fun NavGraphBuilder.composableNestedGraphicPacks( + navController: NavController, + noinline content: @Composable (AnimatedContentScope.(NavBackStackEntry, U) -> Unit), +) { + composable { backStackEntry -> + val previousBackStackEntry = + navController.previousBackStackEntry ?: return@composable + val graphicPackNode = + viewModel(previousBackStackEntry).graphicPackNode + if (graphicPackNode == null || graphicPackNode !is U) { + return@composable + } + content(backStackEntry, graphicPackNode) + } +} + +@Composable +fun GraphicPacksNav( + parentNavBack: () -> Unit, +) { + val navController = rememberNavController() + + fun navigateBack() { + if (!navController.popBackStack()) { + parentNavBack() + } + } + + fun graphicPacksNavigate(graphicPackNode: GraphicPackNode) { + when (graphicPackNode) { + is GraphicPackSectionNode -> navController.navigate(GraphicPackRoutes.GraphicPackSectionScreenRoute) + is GraphicPackDataNode -> navController.navigate(GraphicPackRoutes.GraphicPackDataScreenRoute) + } + } + + NavHost( + navController = navController, + startDestination = GraphicPackRoutes.GraphicPacksRootSectionRoute + ) { + composable { backStackEntry -> + val graphicPackViewModel: GraphicPackViewModel = viewModel(backStackEntry) + GraphicPacksRootSectionScreen( + navigateBack = ::navigateBack, + graphicPackNodeNavigate = { + graphicPackViewModel.graphicPackNode = it + graphicPacksNavigate(it) + } + ) + } + composableNestedGraphicPacks( + navController + ) { backStackEntry, graphicPackNode -> + val graphicPacksViewModel: GraphicPackViewModel = viewModel(backStackEntry) + GraphicPacksSectionScreen( + navigateBack = ::navigateBack, + graphicPackNodeNavigate = { + graphicPacksViewModel.graphicPackNode = it + graphicPacksNavigate(it) + }, + graphicPackSectionNode = graphicPackNode, + ) + } + composableNestedGraphicPacks( + navController + ) { backStackEntry, graphicPackNode -> + val graphicPackDataViewModel: GraphicPackDataViewModel = viewModel( + viewModelStoreOwner = backStackEntry, + factory = GraphicPackDataViewModel.Factory, + extras = MutableCreationExtras().apply { + set(GraphicPackDataViewModel.GRAPHIC_PACK_KEY, graphicPackNode) + } + ) + GraphicPackDataScreen( + navigateBack = ::navigateBack, + graphicPackDataViewModel = graphicPackDataViewModel, + ) + } + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksDownloader.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksDownloader.kt new file mode 100644 index 00000000..ba5bb846 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksDownloader.kt @@ -0,0 +1,116 @@ +package info.cemu.cemu.graphicpacks + +import android.content.Context +import info.cemu.cemu.nativeinterface.NativeGraphicPacks +import info.cemu.cemu.zip.unzip +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.executeAsync +import org.json.JSONObject +import java.io.File +import java.io.IOException +import kotlin.io.path.div +import kotlin.io.path.readText + +sealed class GraphicPacksDownloadStatus { + data object CheckingForUpdates : GraphicPacksDownloadStatus() + data object NoUpdatesAvailable : GraphicPacksDownloadStatus() + data object Downloading : GraphicPacksDownloadStatus() + data object FinishedDownloading : GraphicPacksDownloadStatus() + data object Error : GraphicPacksDownloadStatus() + data object Canceled : GraphicPacksDownloadStatus() +} + +class GraphicPacksDownloader { + private fun getCurrentVersion(graphicPacksDir: File): String? { + val graphicPacksVersionFile = + graphicPacksDir.toPath() / "downloadedGraphicPacks" / "version.txt" + return try { + graphicPacksVersionFile.readText() + } catch (ignored: IOException) { + null + } + } + + suspend fun download( + context: Context, + updateStatus: suspend (GraphicPacksDownloadStatus) -> Unit + ) { + val graphicPacksRootDir = context.getExternalFilesDir(null) + if (graphicPacksRootDir == null) { + updateStatus(GraphicPacksDownloadStatus.Error) + return + } + val graphicPacksDirPath = graphicPacksRootDir.toPath() / "graphicPacks" + checkForNewUpdate(graphicPacksDirPath.toFile(), updateStatus) + } + + private suspend fun checkForNewUpdate( + graphicPacksDir: File, + updateStatus: suspend (GraphicPacksDownloadStatus) -> Unit + ) { + updateStatus(GraphicPacksDownloadStatus.CheckingForUpdates) + val request = Request.Builder() + .url(GITHUB_RELEASES_API_URL) + .build() + Client.newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + if (!response.isSuccessful) { + updateStatus(GraphicPacksDownloadStatus.Error) + return@withContext + } + val json = JSONObject(response.body.string()) + val version = json.getString("name") + if (getCurrentVersion(graphicPacksDir) == version) { + updateStatus(GraphicPacksDownloadStatus.NoUpdatesAvailable) + return@withContext + } + val downloadUrl = json.getJSONArray("assets") + .getJSONObject(0) + .getString("browser_download_url") + downloadNewUpdate(graphicPacksDir, downloadUrl, version, updateStatus) + } + } + } + + private suspend fun downloadNewUpdate( + graphicPacksDir: File, + downloadUrl: String, + version: String, + updateStatus: suspend (GraphicPacksDownloadStatus) -> Unit + ) { + updateStatus(GraphicPacksDownloadStatus.Downloading) + val request = Request.Builder() + .url(downloadUrl) + .build() + Client.newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + if (!response.isSuccessful) { + updateStatus(GraphicPacksDownloadStatus.Error) + return@withContext + } + val graphicPacksTempDir = graphicPacksDir.resolve("downloadedGraphicPacksTemp") + graphicPacksTempDir.deleteRecursively() + unzip( + response.body.byteStream(), + graphicPacksTempDir.path.toString() + ) + graphicPacksTempDir.resolve("version.txt").writeText(version) + val downloadedGraphicPacksDir = + graphicPacksDir.resolve("downloadedGraphicPacks") + downloadedGraphicPacksDir.deleteRecursively() + graphicPacksTempDir.renameTo(downloadedGraphicPacksDir) + NativeGraphicPacks.refreshGraphicPacks() + updateStatus(GraphicPacksDownloadStatus.FinishedDownloading) + } + } + } + + companion object { + private val Client = OkHttpClient() + private const val GITHUB_RELEASES_API_URL = + "https://api.github.com/repos/cemu-project/cemu_graphic_packs/releases/latest" + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..3e003c6d --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksListViewModel.kt @@ -0,0 +1,148 @@ +package info.cemu.cemu.graphicpacks + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import info.cemu.cemu.nativeinterface.NativeGameTitles +import info.cemu.cemu.nativeinterface.NativeGraphicPacks +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import java.util.regex.Pattern + +class GraphicPacksListViewModel : ViewModel() { + private val installedTitleIds = NativeGameTitles.getInstalledGamesTitleIds() + private var rootNode = GraphicPackSectionNode() + + private val _installedOnly = MutableStateFlow(true) + val installedOnly = _installedOnly.asStateFlow() + fun setInstalledOnly(installedOnly: Boolean) { + _installedOnly.value = installedOnly + } + + private val _downloadStatus = MutableStateFlow(null) + private suspend fun updateDownloadStatus(status: GraphicPacksDownloadStatus?) { + _downloadStatus.first { it == null } + _downloadStatus.value = status + } + + val downloadStatus = _downloadStatus.asStateFlow() + + @Volatile + private var downloadJob: Job? = null + fun downloadNewUpdate(context: Context) { + if (_downloadStatus.value != null) return + downloadJob = viewModelScope.launch { + try { + GraphicPacksDownloader.download(context) { updateDownloadStatus(it) } + refreshGraphicPacks() + } catch (exception: Exception) { + updateDownloadStatus(GraphicPacksDownloadStatus.Error) + } + } + } + + fun downloadStatusRead() { + _downloadStatus.value = null + } + + fun cancelDownload() { + downloadJob?.cancel(CancellationException("Canceled by user")) + downloadJob = null + viewModelScope.launch { + updateDownloadStatus(GraphicPacksDownloadStatus.Canceled) + } + } + + private val _filterText = MutableStateFlow("") + val filterText: StateFlow = _filterText + fun setFilterText(filterText: String) { + _filterText.value = filterText + } + + private val filterPattern = filterText.map { filterText -> + if (filterText.isBlank()) { + return@map null + } + return@map buildString { + filterText.trim() + .split(" ".toRegex()) + .forEach { append("(?=.*" + Pattern.quote(it) + ")") } + append(".*") + }.toPattern(Pattern.CASE_INSENSITIVE) + } + + private val _graphicPackDataNodes = MutableStateFlow>(emptyList()) + val graphicPackDataNodes: StateFlow> = + combine( + _graphicPackDataNodes, + installedOnly, + filterPattern + ) { graphicPackNodes, installedOnly, pattern -> + if (!installedOnly && pattern == null) { + return@combine graphicPackNodes + } + if (pattern != null) { + return@combine graphicPackNodes.filter { + pattern.matcher(it.path).matches() && (it.titleIdInstalled || !installedOnly) + } + } + return@combine graphicPackNodes.filter { it.titleIdInstalled } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + emptyList() + ) + + private val _graphicPackNodes = MutableStateFlow>(emptyList()) + val graphicPackNodes: StateFlow> = + installedOnly.combine(_graphicPackNodes) { installedOnly, graphicPackNodes -> + if (installedOnly) { + return@combine graphicPackNodes.filter { it.titleIdInstalled } + } + return@combine graphicPackNodes + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + emptyList() + ) + + init { + refreshGraphicPacks() + } + + private fun MutableList.fillWithDataNodes(graphicPackSectionNode: GraphicPackSectionNode): MutableList { + for (node in graphicPackSectionNode.children) { + when (node) { + is GraphicPackSectionNode -> fillWithDataNodes(node) + is GraphicPackDataNode -> add(node) + } + } + return this + } + + private fun refreshGraphicPacks() { + rootNode = GraphicPackSectionNode().apply { + NativeGraphicPacks.getGraphicPackBasicInfos().forEach { + val hasTitleInstalled = it.titleIds.any { titleId -> titleId in installedTitleIds } + addGraphicPackDataByTokens(it, hasTitleInstalled) + } + sort() + } + _graphicPackDataNodes.value = + mutableListOf().fillWithDataNodes(rootNode) + _graphicPackNodes.value = rootNode.children.toList() + } + + companion object { + private val GraphicPacksDownloader = GraphicPacksDownloader() + } +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksScreen.kt new file mode 100644 index 00000000..d792d749 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksScreen.kt @@ -0,0 +1,464 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package info.cemu.cemu.graphicpacks + +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +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 +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.lifecycle.viewmodel.compose.viewModel +import info.cemu.cemu.R +import info.cemu.cemu.guicore.DefaultAppBarTitle +import info.cemu.cemu.guicore.ScreenContentLazy +import info.cemu.cemu.guicore.SearchToolbarInput +import info.cemu.cemu.guicore.SingleSelection +import kotlinx.coroutines.launch + + +@Composable +fun GraphicPacksRootSectionScreen( + navigateBack: () -> Unit, + graphicPackNodeNavigate: (GraphicPackNode) -> Unit, + graphicPacksListViewModel: GraphicPacksListViewModel = viewModel(), +) { + val graphicPackNodes by graphicPacksListViewModel.graphicPackNodes.collectAsState() + val graphicPackDataNodes by graphicPacksListViewModel.graphicPackDataNodes.collectAsState() + val query by graphicPacksListViewModel.filterText.collectAsState() + val installedOnly by graphicPacksListViewModel.installedOnly.collectAsState() + var showGraphicPackSearch by rememberSaveable { mutableStateOf(false) } + val downloadStatus by graphicPacksListViewModel.downloadStatus.collectAsState() + val snackbarScope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + var downloadDialogText by rememberSaveable { mutableStateOf(null) } + val context = LocalContext.current + + downloadStatus?.let { status -> + downloadDialogText = + downloadStatusToDialogTextStringId(status)?.let { stringResource(it) } + LaunchedEffect(status) { + graphicPacksListViewModel.downloadStatusRead() + + val downloadNotificationText = + downloadStatusToNotificationStringId(status)?.let(context::getString) + ?: return@LaunchedEffect + snackbarScope.launch { + snackbarHostState.currentSnackbarData?.dismiss() + snackbarHostState.showSnackbar(downloadNotificationText) + } + } + } + fun onNavigateBack() { + if (showGraphicPackSearch) { + showGraphicPackSearch = false + } else { + navigateBack() + } + } + + ScreenContentLazy( + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + actions = { + GraphicPacksRootSectionActions( + showMainActions = !showGraphicPackSearch, + onSearchClicked = { + showGraphicPackSearch = true + }, + onDownloadClicked = { graphicPacksListViewModel.downloadNewUpdate(context) }, + installedOnlyChecked = installedOnly, + installedOnlyValueChange = graphicPacksListViewModel::setInstalledOnly, + ) + }, + appBarTitle = { + Box( + modifier = Modifier + .height(IntrinsicSize.Min) + .padding(8.dp), + ) { + if (showGraphicPackSearch) { + SearchToolbarInput( + value = query, + onValueChange = graphicPacksListViewModel::setFilterText, + hint = stringResource(R.string.search_graphic_packs), + ) + } else { + DefaultAppBarTitle(stringResource(R.string.graphic_packs)) + } + } + }, + navigateBack = ::onNavigateBack, + ) { + if (showGraphicPackSearch) { + graphicPackDataSearchItems( + nodes = graphicPackDataNodes, + onClick = graphicPackNodeNavigate, + ) + } else { + graphicPackSectionItems( + nodes = graphicPackNodes, + onClick = graphicPackNodeNavigate, + ) + } + } + if (downloadDialogText != null) { + GraphicPacksDownloadDialog( + onCancelRequest = { + graphicPacksListViewModel.cancelDownload() + }, + text = downloadDialogText!!, + ) + } +} + +@StringRes +private fun downloadStatusToDialogTextStringId(downloadStatus: GraphicPacksDownloadStatus?): Int? = + when (downloadStatus) { + GraphicPacksDownloadStatus.CheckingForUpdates -> R.string.checking_version_download_text + GraphicPacksDownloadStatus.Downloading -> R.string.graphic_packs_download_text + else -> null + } + +@StringRes +private fun downloadStatusToNotificationStringId(downloadStatus: GraphicPacksDownloadStatus?): Int? = + when (downloadStatus) { + GraphicPacksDownloadStatus.Error -> R.string.download_graphic_packs_error_text + GraphicPacksDownloadStatus.FinishedDownloading -> R.string.download_graphic_packs_finish_text + GraphicPacksDownloadStatus.NoUpdatesAvailable -> R.string.graphic_packs_no_updates_text + else -> null + } + +@Composable +fun GraphicPacksDownloadDialog( + onCancelRequest: () -> Unit, + text: String, +) { + AlertDialog( + title = { + Text(text = stringResource(R.string.graphic_packs_download_dialog_title)) + }, + text = { + Column(modifier = Modifier.padding(vertical = 8.dp)) { + Text( + text = text, + modifier = Modifier.padding(bottom = 16.dp) + ) + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + ) + } + }, + onDismissRequest = {}, + confirmButton = { + TextButton( + onClick = { + onCancelRequest() + } + ) { + Text(stringResource(R.string.cancel)) + } + } + ) +} + +@Composable +fun GraphicPacksRootSectionActions( + showMainActions: Boolean, + onSearchClicked: () -> Unit, + onDownloadClicked: () -> Unit, + installedOnlyChecked: Boolean, + installedOnlyValueChange: (Boolean) -> Unit, +) { + if (showMainActions) { + IconButton( + onClick = onSearchClicked + ) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = stringResource(R.string.search_graphic_packs) + ) + } + IconButton( + onClick = onDownloadClicked + ) { + Icon( + painter = painterResource(R.drawable.ic_download), + contentDescription = stringResource(R.string.download_graphic_packs) + ) + } + } + var showMoreOptions by rememberSaveable { mutableStateOf(false) } + IconButton( + onClick = { showMoreOptions = true } + ) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = stringResource(R.string.more_options) + ) + } + DropdownMenu( + expanded = showMoreOptions, + onDismissRequest = { showMoreOptions = false } + ) { + DropdownMenuItem( + onClick = { + installedOnlyValueChange(!installedOnlyChecked) + }, + text = { + Text(text = stringResource(R.string.graphic_packs_installed_only)) + }, + trailingIcon = { + Checkbox( + checked = installedOnlyChecked, + onCheckedChange = null + ) + } + ) + } +} + +fun LazyListScope.graphicPackDataSearchItems( + nodes: List, + onClick: (GraphicPackDataNode) -> Unit, +) { + items(nodes) { + Row( + modifier = Modifier + .animateItem() + .clickable(onClick = dropUnlessResumed { onClick(it) }) + .padding(8.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + GraphicPackDataListItemIcon(it.enabled) + Column { + Text( + modifier = Modifier.padding(horizontal = 8.dp), + text = it.name ?: "", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + modifier = Modifier.padding(horizontal = 8.dp), + text = it.path, + ) + } + } + } +} + +@Composable +fun GraphicPacksSectionScreen( + navigateBack: () -> Unit, + graphicPackNodeNavigate: (GraphicPackNode) -> Unit, + graphicPackSectionNode: GraphicPackSectionNode, +) { + val appBarText = graphicPackSectionNode.name ?: stringResource(R.string.graphic_packs) + ScreenContentLazy( + appBarText = appBarText, + navigateBack = navigateBack, + ) { + graphicPackSectionItems( + nodes = graphicPackSectionNode.children, + onClick = graphicPackNodeNavigate, + ) + } +} + +@Composable +fun GraphicPackDataScreen( + navigateBack: () -> Unit, + graphicPackDataViewModel: GraphicPackDataViewModel, +) { + val appBarText = graphicPackDataViewModel.name ?: stringResource(R.string.graphic_packs) + val enabled by graphicPackDataViewModel.enabled.collectAsState() + val presets by graphicPackDataViewModel.presets.collectAsState() + + ScreenContentLazy( + appBarText = appBarText, + navigateBack = navigateBack, + ) { + item { + Row( + modifier = Modifier.padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = stringResource(R.string.graphic_pack_enabled_checkbox_label)) + Switch( + modifier = Modifier.padding(horizontal = 8.dp), + checked = enabled, + onCheckedChange = graphicPackDataViewModel::setEnabled, + ) + } + } + item { + Text( + modifier = Modifier.padding(8.dp), + text = graphicPackDataViewModel.description + ) + } + items(items = presets) { + SingleSelection( + modifier = Modifier.animateItem(), + label = it.category ?: stringResource(R.string.active_preset_category), + choices = it.presets, + choice = it.activePreset, + onChoiceChanged = { activePreset -> + graphicPackDataViewModel.setActivePreset( + it.index, + activePreset + ) + } + ) + } + } +} + + +private fun LazyListScope.graphicPackSectionItems( + nodes: List, + onClick: (GraphicPackNode) -> Unit, +) { + items( + items = nodes, + ) { + GraphicPackListItem( + label = it.name, + onClick = dropUnlessResumed { onClick(it) }, + modifier = Modifier.animateItem(), + ) { + when (it) { + is GraphicPackSectionNode -> GraphicPackSectionListItemIcon(it.enabledGraphicPacksCount) + is GraphicPackDataNode -> GraphicPackDataListItemIcon(it.enabled) + } + } + } +} + +@Composable +fun GraphicPackDataListItemIcon(isEnabled: Boolean) { + GraphicPackListItemIcon( + painter = painterResource(R.drawable.ic_package_2), + showExtraInfo = isEnabled, + ) { + Icon( + modifier = Modifier + .background(MaterialTheme.colorScheme.primary, CircleShape) + .size(16.dp), + imageVector = Icons.Filled.Check, + tint = contentColorFor(MaterialTheme.colorScheme.primary), + contentDescription = stringResource(R.string.graphic_pack_is_enabled), + ) + } +} + +@Composable +fun GraphicPackSectionListItemIcon(numberOfEnabledPacks: Int) { + GraphicPackListItemIcon( + painter = painterResource(R.drawable.ic_lists), + showExtraInfo = numberOfEnabledPacks > 0, + ) { + Text( + textAlign = TextAlign.Center, + text = if (numberOfEnabledPacks < 99) numberOfEnabledPacks.toString() else "99+", + modifier = Modifier + .background(MaterialTheme.colorScheme.primary, RoundedCornerShape(2.dp)) + .padding(horizontal = 2.dp), + color = contentColorFor(MaterialTheme.colorScheme.primary), + ) + } +} + +@Composable +fun GraphicPackListItemIcon( + painter: Painter, + showExtraInfo: Boolean, + extraInfoContent: @Composable () -> Unit, +) { + Box { + Icon( + modifier = Modifier.size(28.dp), + painter = painter, + contentDescription = null, + ) + if (showExtraInfo) { + Box(modifier = Modifier.align(Alignment.BottomEnd)) { + extraInfoContent() + } + } + } +} + +@Composable +fun GraphicPackListItem( + label: String?, + onClick: () -> Unit, + modifier: Modifier, + icon: @Composable () -> Unit, +) { + Row( + modifier = modifier + .clickable(onClick = onClick) + .padding(8.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + icon() + Text( + modifier = Modifier + .weight(1.0f) + .padding(horizontal = 8.dp), + text = label ?: "", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/BaseSelectionAdapter.kt b/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/BaseSelectionAdapter.kt deleted file mode 100644 index 1ec69f5a..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/BaseSelectionAdapter.kt +++ /dev/null @@ -1,39 +0,0 @@ -package info.cemu.cemu.guibasecomponents - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.BaseAdapter -import com.google.android.material.radiobutton.MaterialRadioButton -import info.cemu.cemu.R - -abstract class BaseSelectionAdapter : BaseAdapter() { - @JvmField - protected var selectedPosition: Int = 0 - - open fun setSelectedValue(selectedValue: T) { - selectedPosition = getPositionOf(selectedValue) - } - - abstract fun getPositionOf(value: T): Int - - protected abstract fun setRadioButtonText(radioButton: MaterialRadioButton, position: Int) - - abstract override fun getItem(position: Int): T - - override fun getItemId(position: Int): Long { - return position.toLong() - } - - override fun getView(position: Int, view: View?, viewGroup: ViewGroup): View { - return (view ?: LayoutInflater.from(viewGroup.context) - .inflate(R.layout.layout_single_selection_item, viewGroup, false)) - .apply { - val radioButton = - findViewById(R.id.single_selection_item_radio_button) - radioButton.isEnabled = isEnabled(position) - setRadioButtonText(radioButton, position) - radioButton.isChecked = position == selectedPosition - } - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/ButtonRecyclerViewItem.kt b/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/ButtonRecyclerViewItem.kt deleted file mode 100644 index fc1f080d..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/ButtonRecyclerViewItem.kt +++ /dev/null @@ -1,44 +0,0 @@ -package info.cemu.cemu.guibasecomponents - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import info.cemu.cemu.R - -class ButtonRecyclerViewItem( - private val text: String, - private val description: String, - private val onButtonClickListener: OnButtonClickListener? -) : - RecyclerViewItem { - fun interface OnButtonClickListener { - fun onButtonClick() - } - - private class ButtonViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var text: TextView = - itemView.findViewById(R.id.button_text) - var description: TextView = - itemView.findViewById(R.id.button_description) - } - - override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { - return ButtonViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.layout_button, parent, false) - ) - } - - override fun onBindViewHolder( - viewHolder: RecyclerView.ViewHolder, - adapter: RecyclerView.Adapter - ) { - val buttonViewHolder = viewHolder as ButtonViewHolder - buttonViewHolder.text.text = text - buttonViewHolder.description.text = description - buttonViewHolder.itemView.setOnClickListener { view: View? -> - onButtonClickListener?.onButtonClick() - } - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/CheckboxRecyclerViewItem.kt b/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/CheckboxRecyclerViewItem.kt deleted file mode 100644 index 860dd1af..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/CheckboxRecyclerViewItem.kt +++ /dev/null @@ -1,50 +0,0 @@ -package info.cemu.cemu.guibasecomponents - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.checkbox.MaterialCheckBox -import info.cemu.cemu.R - -class CheckboxRecyclerViewItem( - private val label: String, - private val description: String, - private var checked: Boolean, - private val onCheckedChangeListener: OnCheckedChangeListener? -) : - RecyclerViewItem { - interface OnCheckedChangeListener { - fun onCheckChanged(checked: Boolean) - } - - private class CheckBoxViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var label: TextView = - itemView.findViewById(R.id.checkbox_label) - var description: TextView = - itemView.findViewById(R.id.checkbox_description) - var checkBox: MaterialCheckBox = itemView.findViewById(R.id.checkbox) - } - - override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { - return CheckBoxViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.layout_checkbox, parent, false) - ) - } - - override fun onBindViewHolder( - viewHolder: RecyclerView.ViewHolder, - adapter: RecyclerView.Adapter - ) { - val checkBoxViewHolder = viewHolder as CheckBoxViewHolder - checkBoxViewHolder.label.text = label - checkBoxViewHolder.description.text = description - checkBoxViewHolder.checkBox.isChecked = checked - checkBoxViewHolder.itemView.setOnClickListener { view: View? -> - checked = !checkBoxViewHolder.checkBox.isChecked - checkBoxViewHolder.checkBox.isChecked = checked - onCheckedChangeListener?.onCheckChanged(checked) - } - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/FilterableRecyclerViewAdapter.kt b/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/FilterableRecyclerViewAdapter.kt deleted file mode 100644 index fe378c47..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/FilterableRecyclerViewAdapter.kt +++ /dev/null @@ -1,58 +0,0 @@ -package info.cemu.cemu.guibasecomponents - -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import java.util.function.Predicate -import java.util.stream.Collectors - -class FilterableRecyclerViewAdapter : - RecyclerView.Adapter() { - private var predicate: Predicate? = null - private val recyclerViewItems: MutableList = ArrayList() - private var filteredRecyclerViewItems: MutableList = ArrayList() - - fun addRecyclerViewItem(recyclerViewItem: T) { - recyclerViewItems.add(recyclerViewItem) - if (predicate == null || predicate!!.test(recyclerViewItem)) { - filteredRecyclerViewItems.add(recyclerViewItem) - notifyItemInserted(filteredRecyclerViewItems.size - 1) - } - } - - fun setPredicate(predicate: Predicate) { - this.predicate = predicate - val oldSize = filteredRecyclerViewItems.size - filteredRecyclerViewItems = - recyclerViewItems.stream().filter(predicate).collect(Collectors.toList()) - if (oldSize != filteredRecyclerViewItems.size) { - notifyDataSetChanged() - } - } - - fun clearPredicate() { - val oldSize = filteredRecyclerViewItems.size - filteredRecyclerViewItems = ArrayList(recyclerViewItems) - if (oldSize != filteredRecyclerViewItems.size) { - notifyDataSetChanged() - } - } - - override fun onCreateViewHolder(parent: ViewGroup, position: Int): RecyclerView.ViewHolder { - return filteredRecyclerViewItems[position]!!.onCreateViewHolder(parent) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - filteredRecyclerViewItems[position]!!.onBindViewHolder(holder, this) - } - - override fun getItemCount(): Int { - return filteredRecyclerViewItems.size - } - - fun clearRecyclerViewItems() { - val size = filteredRecyclerViewItems.size - filteredRecyclerViewItems.clear() - recyclerViewItems.clear() - notifyItemRangeRemoved(0, size) - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/GenericRecyclerViewAdapter.kt b/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/GenericRecyclerViewAdapter.kt deleted file mode 100644 index 3902f7be..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/GenericRecyclerViewAdapter.kt +++ /dev/null @@ -1,60 +0,0 @@ -package info.cemu.cemu.guibasecomponents - -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import java.util.Objects -import java.util.stream.IntStream - -class GenericRecyclerViewAdapter : RecyclerView.Adapter() { - @JvmRecord - private data class RecyclerViewItemTuple(val viewTypeId: Int, val item: RecyclerViewItem) - - private val recyclerViewItems: MutableList = ArrayList() - private var currentViewTypeId = 0 - private val recyclerViewItemsViewTypeHasMap = HashMap() - - fun addRecyclerViewItem(recyclerViewItem: RecyclerViewItem) { - val viewTypeId = currentViewTypeId++ - recyclerViewItems.add(RecyclerViewItemTuple(viewTypeId, recyclerViewItem)) - val position = recyclerViewItems.size - 1 - recyclerViewItemsViewTypeHasMap[viewTypeId] = position - notifyItemInserted(recyclerViewItems.size - 1) - } - - fun removeRecyclerViewItem(recyclerViewItem: RecyclerViewItem) { - val position = IntStream.range(0, recyclerViewItems.size) - .filter { index: Int -> recyclerViewItems[index].item === recyclerViewItem } - .findFirst() - if (position.isEmpty) { - return - } - val itemTuple = recyclerViewItems[position.asInt] - recyclerViewItemsViewTypeHasMap.remove(itemTuple.viewTypeId) - recyclerViewItems.remove(itemTuple) - notifyItemRemoved(position.asInt) - } - - fun clearRecyclerViewItems() { - val itemsCount = recyclerViewItems.size - recyclerViewItems.clear() - recyclerViewItemsViewTypeHasMap.clear() - notifyItemRangeRemoved(0, itemsCount) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewTypeId: Int): RecyclerView.ViewHolder { - val position = recyclerViewItemsViewTypeHasMap[viewTypeId]!! - return recyclerViewItems[position].item.onCreateViewHolder(parent) - } - - override fun getItemViewType(position: Int): Int { - return recyclerViewItems[position].viewTypeId - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - recyclerViewItems[position].item.onBindViewHolder(holder, this) - } - - override fun getItemCount(): Int { - return recyclerViewItems.size - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/HeaderRecyclerViewItem.kt b/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/HeaderRecyclerViewItem.kt deleted file mode 100644 index 46425ca2..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/HeaderRecyclerViewItem.kt +++ /dev/null @@ -1,47 +0,0 @@ -package info.cemu.cemu.guibasecomponents - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import info.cemu.cemu.R -import java.util.Optional - -class HeaderRecyclerViewItem : RecyclerViewItem { - private class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var header: TextView = - itemView.findViewById(R.id.textview_header) - } - - private val headerResourceIdText: Optional - private val headerText: String? - - constructor(headerResourceIdText: Int) { - this.headerResourceIdText = Optional.of(headerResourceIdText) - this.headerText = null - } - - constructor(headerText: String?) { - this.headerResourceIdText = Optional.empty() - this.headerText = headerText - } - - override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { - return HeaderViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.layout_header, parent, false) - ) - } - - override fun onBindViewHolder( - viewHolder: RecyclerView.ViewHolder, - adapter: RecyclerView.Adapter - ) { - val headerViewHolder = viewHolder as HeaderViewHolder - if (headerResourceIdText.isEmpty) { - headerViewHolder.header.text = headerText - return - } - headerViewHolder.header.setText(headerResourceIdText.get()) - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/RecyclerViewItem.kt b/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/RecyclerViewItem.kt deleted file mode 100644 index 52782141..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/RecyclerViewItem.kt +++ /dev/null @@ -1,13 +0,0 @@ -package info.cemu.cemu.guibasecomponents - -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView - -interface RecyclerViewItem { - fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder - - fun onBindViewHolder( - viewHolder: RecyclerView.ViewHolder, - adapter: RecyclerView.Adapter - ) -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/SelectionAdapter.kt b/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/SelectionAdapter.kt deleted file mode 100644 index 786101a5..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/SelectionAdapter.kt +++ /dev/null @@ -1,46 +0,0 @@ -package info.cemu.cemu.guibasecomponents - -import android.widget.TextView -import com.google.android.material.radiobutton.MaterialRadioButton -import java.util.function.Consumer -import java.util.stream.IntStream - -open class SelectionAdapter : BaseSelectionAdapter { - class ChoiceItem(val setTextForChoice: Consumer, val choiceValue: T) - - @JvmField - protected var choiceItems: List> = ArrayList() - - constructor() : super() - - constructor(choiceItems: List>, selectedValue: T) : super() { - this.choiceItems = choiceItems - setSelectedValue(selectedValue) - } - - final override fun setSelectedValue(selectedValue: T) { - selectedPosition = getPositionOf(selectedValue) - } - - override fun getPositionOf(value: T): Int { - val optionalInt = IntStream.range(0, choiceItems.size) - .filter { position: Int -> choiceItems[position].choiceValue == value } - .findFirst() - if (optionalInt.isPresent) { - return optionalInt.asInt - } - throw IllegalArgumentException("value $value was not found in the list of choiceItems") - } - - override fun setRadioButtonText(radioButton: MaterialRadioButton, position: Int) { - choiceItems[position].setTextForChoice.accept(radioButton) - } - - override fun getCount(): Int { - return choiceItems.size - } - - override fun getItem(position: Int): T { - return choiceItems[position].choiceValue - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/SimpleButtonRecyclerViewItem.kt b/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/SimpleButtonRecyclerViewItem.kt deleted file mode 100644 index 925700fd..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/SimpleButtonRecyclerViewItem.kt +++ /dev/null @@ -1,41 +0,0 @@ -package info.cemu.cemu.guibasecomponents - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import info.cemu.cemu.R - -class SimpleButtonRecyclerViewItem( - private val text: String, - private val onButtonClickListener: OnButtonClickListener? -) : - RecyclerViewItem { - fun interface OnButtonClickListener { - fun onButtonClick() - } - - private class SimpleButtonViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var text: TextView = - itemView.findViewById(R.id.simple_button_text) - } - - override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { - return SimpleButtonViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.layout_simple_button, parent, false) - ) - } - - override fun onBindViewHolder( - viewHolder: RecyclerView.ViewHolder, - adapter: RecyclerView.Adapter - ) { - val buttonViewHolder = viewHolder as SimpleButtonViewHolder? - buttonViewHolder!!.text.text = text - buttonViewHolder.itemView.setOnClickListener { view: View? -> - onButtonClickListener?.onButtonClick() - } - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/SingleSelectionRecyclerViewItem.kt b/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/SingleSelectionRecyclerViewItem.kt deleted file mode 100644 index a05667b8..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/SingleSelectionRecyclerViewItem.kt +++ /dev/null @@ -1,110 +0,0 @@ -package info.cemu.cemu.guibasecomponents - -import android.content.DialogInterface -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.appcompat.app.AlertDialog -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import info.cemu.cemu.R -import info.cemu.cemu.guibasecomponents.SelectionAdapter.ChoiceItem -import java.util.function.Function -import java.util.stream.Collectors - -class SingleSelectionRecyclerViewItem( - private val label: String?, - private var description: String?, - private val selectionAdapter: BaseSelectionAdapter, - private var onItemSelectedListener: OnItemSelectedListener? -) : - RecyclerViewItem { - private class SingleSelectionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var label: TextView = - itemView.findViewById(R.id.selection_label) - var description: TextView = - itemView.findViewById(R.id.selection_description) - } - - fun interface OnItemSelectedListener { - fun onItemSelected( - selectedValue: T, - selectionRecyclerViewItem: SingleSelectionRecyclerViewItem - ) - } - - fun interface OnChoiceSelectedListener { - fun onChoiceSelected(selectedValue: T) - } - - private var singleSelectionViewHolder: SingleSelectionViewHolder? = null - private var recyclerViewAdapter: RecyclerView.Adapter? = null - private var selectAlertDialog: AlertDialog? = null - - constructor( - label: String?, - currentChoice: T, - choicesList: List, - choiceValueToNameFunction: (T) -> String, - onChoiceSelectedListener: OnChoiceSelectedListener - ) : this( - label, - choiceValueToNameFunction(currentChoice), - SelectionAdapter( - choicesList.map { choice: T -> - ChoiceItem({ t: TextView -> - t.text = choiceValueToNameFunction(choice) - }, choice) - }.toList(), - currentChoice - ), - { selectedValue: T, selectionRecyclerViewItem: SingleSelectionRecyclerViewItem -> - onChoiceSelectedListener.onChoiceSelected(selectedValue) - selectionRecyclerViewItem.setDescription(choiceValueToNameFunction(selectedValue)) - }) - - fun setDescription(description: String?) { - this.description = description - recyclerViewAdapter!!.notifyItemChanged(singleSelectionViewHolder!!.adapterPosition) - } - - override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { - return SingleSelectionViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.layout_single_selection, parent, false) - ) - } - - override fun onBindViewHolder( - viewHolder: RecyclerView.ViewHolder, - adapter: RecyclerView.Adapter - ) { - this.recyclerViewAdapter = adapter - singleSelectionViewHolder = viewHolder as SingleSelectionViewHolder? - singleSelectionViewHolder!!.label.text = label - singleSelectionViewHolder!!.description.text = description - singleSelectionViewHolder!!.itemView.setOnClickListener { view: View? -> - if (selectAlertDialog == null) { - val builder = MaterialAlertDialogBuilder(viewHolder.itemView.context) - selectAlertDialog = builder.setTitle(label) - .setAdapter(selectionAdapter) { dialogInterface: DialogInterface?, position: Int -> - if (!selectionAdapter.isEnabled(position)) { - return@setAdapter - } - val selectedValue = selectionAdapter.getItem(position) - selectionAdapter.setSelectedValue(selectedValue) - onItemSelectedListener?.onItemSelected( - selectedValue, - this@SingleSelectionRecyclerViewItem - ) - }.setNegativeButton( - R.string.cancel - ) { dialogInterface: DialogInterface, i: Int -> dialogInterface.dismiss() } - .show() - return@setOnClickListener - } - selectAlertDialog!!.show() - } - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/SliderRecyclerViewItem.kt b/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/SliderRecyclerViewItem.kt deleted file mode 100644 index 1dae075e..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/SliderRecyclerViewItem.kt +++ /dev/null @@ -1,85 +0,0 @@ -package info.cemu.cemu.guibasecomponents - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.slider.LabelFormatter -import com.google.android.material.slider.Slider -import info.cemu.cemu.R - -class SliderRecyclerViewItem( - private val label: String, - private val valueFrom: Float, - private val valueTo: Float, - private var currentValue: Float, - private val stepSize: Float, - private val onChangeListener: OnChangeListener?, - private val labelFormatter: LabelFormatter? -) : - RecyclerViewItem { - fun interface OnChangeListener { - fun onChange(value: Float) - } - - private class SliderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var label: TextView = - itemView.findViewById(R.id.slider_label) - var slider: Slider = itemView.findViewById(R.id.slider) - } - - constructor( - label: String, - valueFrom: Float, - valueTo: Float, - currentValue: Float, - onChangeListener: OnChangeListener?, - labelFormatter: LabelFormatter? - ) : this( - label, - valueFrom, - valueTo, - currentValue, - DEFAULT_STEP_SIZE, - onChangeListener, - labelFormatter - ) - - constructor( - label: String, - valueFrom: Float, - valueTo: Float, - currentValue: Float, - onChangeListener: OnChangeListener? - ) : this(label, valueFrom, valueTo, currentValue, DEFAULT_STEP_SIZE, onChangeListener, null) - - override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { - return SliderViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.layout_slider, parent, false) - ) - } - - override fun onBindViewHolder( - viewHolder: RecyclerView.ViewHolder, - adapter: RecyclerView.Adapter - ) { - val sliderViewHolder = viewHolder as SliderViewHolder - sliderViewHolder.label.text = label - if (labelFormatter != null) { - sliderViewHolder.slider.setLabelFormatter(labelFormatter) - } - sliderViewHolder.slider.valueFrom = valueFrom - sliderViewHolder.slider.valueTo = valueTo - sliderViewHolder.slider.stepSize = stepSize - sliderViewHolder.slider.value = currentValue - sliderViewHolder.slider.addOnChangeListener(Slider.OnChangeListener { slider: Slider?, value: Float, fromUser: Boolean -> - currentValue = value - onChangeListener?.onChange(currentValue) - }) - } - - companion object { - private const val DEFAULT_STEP_SIZE = 1.0f - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/StringSelectionAdapter.kt b/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/StringSelectionAdapter.kt deleted file mode 100644 index 561eef86..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/StringSelectionAdapter.kt +++ /dev/null @@ -1,37 +0,0 @@ -package info.cemu.cemu.guibasecomponents - -import com.google.android.material.radiobutton.MaterialRadioButton -import java.util.stream.IntStream - -class StringSelectionAdapter(private val choices: List, selectedValue: String) : - BaseSelectionAdapter() { - init { - setSelectedValue(selectedValue) - } - - override fun setSelectedValue(selectedValue: String) { - selectedPosition = getPositionOf(selectedValue) - } - - override fun getPositionOf(value: String): Int { - val optionalInt = - IntStream.range(0, choices.size).filter { position: Int -> choices[position] == value } - .findFirst() - if (optionalInt.isPresent) { - return optionalInt.asInt - } - throw IllegalArgumentException("value $value was not found in the list of choices") - } - - override fun setRadioButtonText(radioButton: MaterialRadioButton, position: Int) { - radioButton.text = choices[position] - } - - override fun getCount(): Int { - return choices.size - } - - override fun getItem(position: Int): String { - return choices[position] - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/ToggleRecyclerViewItem.kt b/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/ToggleRecyclerViewItem.kt deleted file mode 100644 index cdf1b421..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/guibasecomponents/ToggleRecyclerViewItem.kt +++ /dev/null @@ -1,50 +0,0 @@ -package info.cemu.cemu.guibasecomponents - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.materialswitch.MaterialSwitch -import info.cemu.cemu.R - -class ToggleRecyclerViewItem( - private val label: String, - private val description: String, - private var checked: Boolean, - private val onCheckedChangeListener: OnCheckedChangeListener? -) : - RecyclerViewItem { - fun interface OnCheckedChangeListener { - fun onCheckChanged(checked: Boolean) - } - - private class ToggleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var label: TextView = - itemView.findViewById(R.id.toggle_label) - var description: TextView = - itemView.findViewById(R.id.toggle_description) - var toggle: MaterialSwitch = itemView.findViewById(R.id.toggle) - } - - override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { - return ToggleViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.layout_toggle, parent, false) - ) - } - - override fun onBindViewHolder( - viewHolder: RecyclerView.ViewHolder, - adapter: RecyclerView.Adapter - ) { - val toggleViewHolder = viewHolder as ToggleViewHolder - toggleViewHolder.label.text = label - toggleViewHolder.description.text = description - toggleViewHolder.toggle.isChecked = checked - toggleViewHolder.itemView.setOnClickListener { view: View? -> - checked = !toggleViewHolder.toggle.isChecked - toggleViewHolder.toggle.isChecked = checked - onCheckedChangeListener?.onCheckChanged(checked) - } - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/ActivityContent.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/ActivityContent.kt new file mode 100644 index 00000000..895f860f --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/guicore/ActivityContent.kt @@ -0,0 +1,20 @@ +package info.cemu.cemu.guicore + +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.guicore.theme.CemuTheme + +@Composable +fun ActivityContent(content: @Composable () -> Unit) { + CemuTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + content() + } + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/Button.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/Button.kt new file mode 100644 index 00000000..e170213c --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/guicore/Button.kt @@ -0,0 +1,50 @@ +package info.cemu.cemu.guicore + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun Button(label: String, description: String? = null, onClick: () -> Unit = {}) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + ), + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + ) { + Text( + modifier = Modifier, + text = label, + fontSize = 20.sp, + fontWeight = FontWeight.Medium, + ) + if (description != null) { + Text( + modifier = Modifier.padding(top = 8.dp), + color = LocalContentColor.current.copy(alpha = 0.6f), + text = description, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + ) + } + } + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/FilledSearchToolbar.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/FilledSearchToolbar.kt new file mode 100644 index 00000000..2a380eab --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/guicore/FilledSearchToolbar.kt @@ -0,0 +1,192 @@ +package info.cemu.cemu.guicore + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.PlatformImeOptions +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import info.cemu.cemu.R + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FilledSearchToolbar( + hint: String, + onValueChange: (String) -> Unit, + actions: @Composable RowScope.() -> Unit = {}, +) { + var searchBarActive by remember { mutableStateOf(false) } + var query by remember { mutableStateOf("") } + TopAppBar( + actions = actions, + title = { + Card( + onClick = { searchBarActive = true }, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + ), + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + .padding(8.dp), + shape = RoundedCornerShape(24.dp), + ) { + Box( + modifier = Modifier + .fillMaxHeight() + .padding(8.dp), + contentAlignment = Alignment.CenterStart, + ) { + if (!searchBarActive) { + SearchToolbarHint(hint) + } else { + SearchToolbarInput( + trailingIcon = { + IconButton( + onClick = { + query = "" + onValueChange("") + searchBarActive = false + }, + modifier = Modifier + .padding(horizontal = 8.dp) + .size(32.dp) + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.exit_search_description), + ) + } + }, + value = query, + hint = hint, + onValueChange = { + query = it + onValueChange(query) + }, + ) + } + } + } + }, + ) +} + +@Composable +fun SearchToolbarInput( + value: String, + hint: String, + trailingIcon: @Composable () -> Unit = {}, + onValueChange: (String) -> Unit, +) { + val focusRequester = remember { FocusRequester() } + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + trailingIcon() + BasicTextField( + modifier = Modifier + .fillMaxHeight() + .focusRequester(focusRequester) + .weight(1.0f), + cursorBrush = SolidColor(LocalContentColor.current), + value = value, + keyboardOptions = KeyboardOptions( + platformImeOptions = PlatformImeOptions("flagNoFullscreen|flagNoExtractUi") + ), + singleLine = true, + textStyle = LocalTextStyle.current.copy( + textAlign = TextAlign.Start, + color = LocalContentColor.current, + ), + decorationBox = { innerTextField -> + if (value.isEmpty()) { + SearchToolbarHint( + hint = hint, + color = LocalContentColor.current.copy(alpha = 0.6f) + ) + } + innerTextField() + }, + onValueChange = onValueChange, + ) + if (value.isNotEmpty()) { + IconButton( + onClick = { onValueChange("") }, + modifier = Modifier + .padding(horizontal = 8.dp) + .size(32.dp) + ) { + Icon( + imageVector = Icons.Filled.Clear, + contentDescription = stringResource(R.string.clear_search), + ) + } + } + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + } +} + +@Composable +fun SearchToolbarHint( + hint: String, + color: Color = LocalContentColor.current, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier + .padding(horizontal = 8.dp) + .size(24.dp), + imageVector = Icons.Default.Search, + tint = color, + contentDescription = null, + ) + Text( + text = hint, + fontSize = 16.sp, + color = color, + ) + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/Header.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/Header.kt new file mode 100644 index 00000000..9b111697 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/guicore/Header.kt @@ -0,0 +1,19 @@ +package info.cemu.cemu.guicore + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun Header(text: String?, modifier: Modifier = Modifier) { + Text( + modifier = modifier.padding(horizontal = 8.dp, vertical = 16.dp), + text = text ?: "", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + ) +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/ModifierClickable.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/ModifierClickable.kt new file mode 100644 index 00000000..beac7bc4 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/guicore/ModifierClickable.kt @@ -0,0 +1,33 @@ +package info.cemu.cemu.guicore + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.semantics.Role + +fun Modifier.clickableWithNoRipple( + enabled: Boolean = true, + onClickLabel: String? = null, + role: Role? = null, + onClick: () -> Unit +): Modifier = composed( + inspectorInfo = debugInspectorInfo { + name = "clickable" + properties["enabled"] = enabled + properties["onClickLabel"] = onClickLabel + properties["role"] = role + properties["onClick"] = onClick + } +) { + Modifier.clickable( + enabled = enabled, + onClickLabel = onClickLabel, + onClick = onClick, + role = role, + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/ScreenContent.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/ScreenContent.kt new file mode 100644 index 00000000..1ccce4e7 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/guicore/ScreenContent.kt @@ -0,0 +1,153 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package info.cemu.cemu.guicore + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.dropUnlessResumed +import info.cemu.cemu.R + +@Composable +fun ScreenContent( + appBarText: String, + navigateBack: () -> Unit, + actions: @Composable RowScope.() -> Unit = {}, + contentModifier: Modifier = Modifier.padding(8.dp), + contentVerticalArrangement: Arrangement.Vertical = Arrangement.Top, + contentHorizontalAlignment: Alignment.Horizontal = Alignment.Start, + content: @Composable ColumnScope.() -> Unit, +) { + ScreenContentGeneric( + appBarTitle = { DefaultAppBarTitle(appBarText) }, + navigateBack = navigateBack, + actions = actions, + ) { + Column( + verticalArrangement = contentVerticalArrangement, + horizontalAlignment = contentHorizontalAlignment, + modifier = contentModifier.verticalScroll(rememberScrollState()), + ) { + content() + } + } +} + +@Composable +fun DefaultAppBarTitle(appBarText: String) { + Text( + maxLines = 1, + overflow = TextOverflow.Ellipsis, + text = appBarText, + fontSize = 18.sp, + ) +} + +@Composable +fun ScreenContentLazy( + appBarText: String, + navigateBack: () -> Unit, + snackbarHost: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + contentModifier: Modifier = Modifier.padding(8.dp), + contentVerticalArrangement: Arrangement.Vertical = Arrangement.Top, + content: LazyListScope.() -> Unit, +) { + ScreenContentGeneric( + snackbarHost = snackbarHost, + appBarTitle = { DefaultAppBarTitle(appBarText) }, + navigateBack = navigateBack, + actions = actions, + ) { + LazyColumn( + verticalArrangement = contentVerticalArrangement, + modifier = contentModifier, + ) { + content() + } + } +} + +@Composable +fun ScreenContentLazy( + appBarTitle: @Composable () -> Unit, + navigateBack: () -> Unit, + snackbarHost: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + contentModifier: Modifier = Modifier.padding(8.dp), + contentVerticalArrangement: Arrangement.Vertical = Arrangement.Top, + content: LazyListScope.() -> Unit, +) { + ScreenContentGeneric( + snackbarHost = snackbarHost, + appBarTitle = appBarTitle, + navigateBack = navigateBack, + actions = actions, + ) { + LazyColumn( + verticalArrangement = contentVerticalArrangement, + modifier = contentModifier, + ) { + content() + } + } +} + +@Composable +private fun ScreenContentGeneric( + navigateBack: () -> Unit, + appBarTitle: @Composable () -> Unit, + snackbarHost: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + content: @Composable () -> Unit, +) { + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = snackbarHost, + topBar = { + TopAppBar( + actions = actions, + title = appBarTitle, + navigationIcon = { + IconButton(onClick = dropUnlessResumed { navigateBack() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.nav_back) + ) + } + }, + ) + }, + ) { scaffoldPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(scaffoldPadding) + ) { + content() + } + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/SingleSelection.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/SingleSelection.kt new file mode 100644 index 00000000..799e366b --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/guicore/SingleSelection.kt @@ -0,0 +1,212 @@ +package info.cemu.cemu.guicore + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +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.R + +@Composable +fun SingleSelection( + label: String, + choice: String, + choices: List, + isChoiceEnabled: (String) -> Boolean = { true }, + modifier: Modifier = Modifier, + onChoiceChanged: (String) -> Unit, +) { + SingleSelection( + label = label, + choice = choice, + choices = choices, + modifier = modifier, + choiceToString = { it }, + isChoiceEnabled = isChoiceEnabled, + onChoiceChanged = onChoiceChanged, + ) +} + + +@Composable +fun SingleSelection( + label: String, + initialChoice: () -> T, + choices: List, + choiceToString: @Composable (T) -> String, + isChoiceEnabled: (T) -> Boolean = { true }, + modifier: Modifier = Modifier, + onChoiceChanged: (T) -> Unit, +) { + var choice by rememberSaveable { mutableStateOf(initialChoice()) } + SingleSelection( + label = label, + choice = choice, + choices = choices, + modifier = modifier, + isChoiceEnabled = isChoiceEnabled, + choiceToString = choiceToString, + onChoiceChanged = { newChoice -> + choice = newChoice + onChoiceChanged(newChoice) + }, + ) +} + +@Composable +fun SingleSelection( + label: String, + choice: T, + choices: List, + choiceToString: @Composable (T) -> String, + modifier: Modifier = Modifier, + isChoiceEnabled: (T) -> Boolean = { true }, + onChoiceChanged: (T) -> Unit, +) { + var showSelectDialog by rememberSaveable { mutableStateOf(false) } + Column( + modifier = modifier + .clickable { showSelectDialog = true; } + .fillMaxWidth() + .padding(8.dp), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = label, + modifier = Modifier.padding(vertical = 8.dp), + fontWeight = FontWeight.Medium, + fontSize = 20.sp, + ) + Text( + text = choiceToString(choice), + modifier = Modifier.padding(vertical = 8.dp), + fontSize = 16.sp, + ) + } + if (showSelectDialog) { + SelectDialog( + label = label, + currentChoice = choice, + choices = choices, + choiceToString = choiceToString, + onDismissRequest = { showSelectDialog = false }, + isChoiceEnabled = isChoiceEnabled, + onChoiceChanged = onChoiceChanged + ) + } +} + +@Composable +fun SelectDialog( + label: String, + currentChoice: T, + choices: List, + isChoiceEnabled: (T) -> Boolean, + choiceToString: @Composable (T) -> String, + onDismissRequest: () -> Unit, + onChoiceChanged: (T) -> Unit, +) { + Dialog(onDismissRequest = onDismissRequest) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), + modifier = Modifier + .sizeIn(maxWidth = 560.dp, maxHeight = 560.dp) + .fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + ) { + Box( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth(), + ) { + Column(modifier = Modifier.fillMaxWidth()) { + Text( + modifier = Modifier.padding(8.dp), + text = label, + fontSize = 24.sp, + ) + Column( + modifier = Modifier + .padding(vertical = 8.dp) + .weight(weight = 1.0f, fill = false) + .verticalScroll(rememberScrollState()), + ) { + choices.forEach { choice -> + val isEnabled = isChoiceEnabled(choice) + Choice( + label = choiceToString(choice), + selected = currentChoice == choice, + isEnabled = isEnabled, + onClick = { + onChoiceChanged(choice) + onDismissRequest() + }, + ) + } + } + TextButton( + onClick = onDismissRequest, + modifier = Modifier + .padding(8.dp) + .align(Alignment.End), + ) { + Text(stringResource(R.string.cancel)) + } + } + } + } + } +} + +@Composable +fun Choice(label: String, selected: Boolean, isEnabled: Boolean, onClick: () -> Unit) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .let { + if (isEnabled) it.clickable { onClick() } + else it + } + .padding(vertical = 16.dp, horizontal = 8.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + enabled = isEnabled, + selected = selected, + onClick = null, + ) + Text( + text = label, + color = if (isEnabled) LocalContentColor.current + else LocalContentColor.current.copy(alpha = 0.6f) + ) + } +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/Slider.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/Slider.kt new file mode 100644 index 00000000..174b526a --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/guicore/Slider.kt @@ -0,0 +1,53 @@ +package info.cemu.cemu.guicore + +import androidx.annotation.IntRange +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +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 androidx.compose.ui.util.fastRoundToInt +import androidx.compose.material3.Slider as MaterialSlider + +@Composable +fun Slider( + label: String, + initialValue: () -> Int, + valueFrom: Int, + valueTo: Int, + @IntRange(from = 0) steps: Int = 0, + labelFormatter: (Int) -> String, + onValueChange: (Int) -> Unit, +) { + var value by rememberSaveable { mutableFloatStateOf(initialValue().toFloat()) } + Row(modifier = Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) { + Column(modifier = Modifier.padding(end = 8.dp)) { + Text( + text = label, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + ) + Text( + text = labelFormatter(value.fastRoundToInt()), + fontWeight = FontWeight.Light, + fontSize = 14.sp, + ) + } + MaterialSlider( + valueRange = valueFrom.toFloat()..valueTo.toFloat(), + steps = steps, + value = value, + onValueChangeFinished = { onValueChange(value.toInt()) }, + onValueChange = { value = it }, + ) + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/Toggle.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/Toggle.kt new file mode 100644 index 00000000..0b475370 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/guicore/Toggle.kt @@ -0,0 +1,65 @@ +package info.cemu.cemu.guicore + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun Toggle( + label: String, + initialCheckedState: () -> Boolean, + onCheckedChanged: (Boolean) -> Unit, + description: String?, +) { + var checked by rememberSaveable { mutableStateOf(initialCheckedState()) } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { + checked = !checked + onCheckedChanged(checked) + }, + ) + { + Column( + modifier = Modifier + .weight(1.0f) + .padding(8.dp), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = label, + fontWeight = FontWeight.Medium, + fontSize = 20.sp, + ) + if (description != null) { + Text( + text = description, + fontSize = 14.sp, + modifier = Modifier.padding(top = 8.dp), + ) + } + } + Switch( + modifier = Modifier.padding(end = 8.dp, top = 8.dp, bottom = 8.dp), + checked = checked, + onCheckedChange = null, + ) + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/enumtostringmapper/native/GameTitlesEnums.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/enumtostringmapper/native/GameTitlesEnums.kt new file mode 100644 index 00000000..ea814774 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/guicore/enumtostringmapper/native/GameTitlesEnums.kt @@ -0,0 +1,26 @@ +package info.cemu.cemu.guicore.enumtostringmapper.native + +import androidx.annotation.StringRes +import info.cemu.cemu.R +import info.cemu.cemu.nativeinterface.NativeGameTitles + +@StringRes +fun cpuModeToStringId(cpuMode: Int): Int = when (cpuMode) { + NativeGameTitles.CPU_MODE_SINGLECOREINTERPRETER -> R.string.cpu_mode_single_core_interpreter + NativeGameTitles.CPU_MODE_SINGLECORERECOMPILER -> R.string.cpu_mode_single_core_recompiler + NativeGameTitles.CPU_MODE_MULTICORERECOMPILER -> R.string.cpu_mode_multi_core_recompiler + else -> R.string.cpu_mode_auto +} + +@StringRes +fun regionToStringId(region: Int): Int = when (region) { + NativeGameTitles.CONSOLE_REGION_JPN -> R.string.console_region_japan + NativeGameTitles.CONSOLE_REGION_USA -> R.string.console_region_usa + NativeGameTitles.CONSOLE_REGION_EUR -> R.string.console_region_europe + NativeGameTitles.CONSOLE_REGION_AUS_DEPR -> R.string.console_region_australia + NativeGameTitles.CONSOLE_REGION_CHN -> R.string.console_region_china + NativeGameTitles.CONSOLE_REGION_KOR -> R.string.console_region_korea + NativeGameTitles.CONSOLE_REGION_TWN -> R.string.console_region_taiwan + NativeGameTitles.CONSOLE_REGION_AUTO -> R.string.console_region_auto + else -> R.string.console_region_many +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/enumtostringmapper/native/InputEnums.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/enumtostringmapper/native/InputEnums.kt new file mode 100644 index 00000000..faade5bb --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/guicore/enumtostringmapper/native/InputEnums.kt @@ -0,0 +1,127 @@ +package info.cemu.cemu.guicore.enumtostringmapper.native + +import androidx.annotation.StringRes +import info.cemu.cemu.R +import info.cemu.cemu.nativeinterface.NativeInput + +@StringRes +fun controllerTypeToStringId(type: Int) = when (type) { + NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED -> R.string.disabled + NativeInput.EMULATED_CONTROLLER_TYPE_VPAD -> R.string.vpad_controller + NativeInput.EMULATED_CONTROLLER_TYPE_PRO -> R.string.pro_controller + NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE -> R.string.wiimote_controller + NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC -> R.string.classic_controller + else -> throw IllegalArgumentException("Invalid controller type: $type") +} + +@StringRes +fun proControllerButtonToStringId(buttonId: Int) = when (buttonId) { + NativeInput.PRO_BUTTON_A -> R.string.button_a + NativeInput.PRO_BUTTON_B -> R.string.button_b + NativeInput.PRO_BUTTON_X -> R.string.button_x + NativeInput.PRO_BUTTON_Y -> R.string.button_y + NativeInput.PRO_BUTTON_L -> R.string.button_l + NativeInput.PRO_BUTTON_R -> R.string.button_r + NativeInput.PRO_BUTTON_ZL -> R.string.button_zl + NativeInput.PRO_BUTTON_ZR -> R.string.button_zr + NativeInput.PRO_BUTTON_PLUS -> R.string.button_plus + NativeInput.PRO_BUTTON_MINUS -> R.string.button_minus + NativeInput.PRO_BUTTON_HOME -> R.string.button_home + NativeInput.PRO_BUTTON_UP -> R.string.button_up + NativeInput.PRO_BUTTON_DOWN -> R.string.button_down + NativeInput.PRO_BUTTON_LEFT -> R.string.button_left + NativeInput.PRO_BUTTON_RIGHT -> R.string.button_right + NativeInput.PRO_BUTTON_STICKL -> R.string.button_stickl + NativeInput.PRO_BUTTON_STICKR -> R.string.button_stickr + NativeInput.PRO_BUTTON_STICKL_UP -> R.string.button_stickl_up + NativeInput.PRO_BUTTON_STICKL_DOWN -> R.string.button_stickl_down + NativeInput.PRO_BUTTON_STICKL_LEFT -> R.string.button_stickl_left + NativeInput.PRO_BUTTON_STICKL_RIGHT -> R.string.button_stickl_right + NativeInput.PRO_BUTTON_STICKR_UP -> R.string.button_stickr_up + NativeInput.PRO_BUTTON_STICKR_DOWN -> R.string.button_stickr_down + NativeInput.PRO_BUTTON_STICKR_LEFT -> R.string.button_stickr_left + NativeInput.PRO_BUTTON_STICKR_RIGHT -> R.string.button_stickr_right + else -> throw IllegalArgumentException("Invalid buttonId $buttonId for Pro controller type") +} + +@StringRes +fun classicControllerButtonToStringId(buttonId: Int) = when (buttonId) { + NativeInput.CLASSIC_BUTTON_A -> R.string.button_a + NativeInput.CLASSIC_BUTTON_B -> R.string.button_b + NativeInput.CLASSIC_BUTTON_X -> R.string.button_x + NativeInput.CLASSIC_BUTTON_Y -> R.string.button_y + NativeInput.CLASSIC_BUTTON_L -> R.string.button_l + NativeInput.CLASSIC_BUTTON_R -> R.string.button_r + NativeInput.CLASSIC_BUTTON_ZL -> R.string.button_zl + NativeInput.CLASSIC_BUTTON_ZR -> R.string.button_zr + NativeInput.CLASSIC_BUTTON_PLUS -> R.string.button_plus + NativeInput.CLASSIC_BUTTON_MINUS -> R.string.button_minus + NativeInput.CLASSIC_BUTTON_HOME -> R.string.button_home + NativeInput.CLASSIC_BUTTON_UP -> R.string.button_up + NativeInput.CLASSIC_BUTTON_DOWN -> R.string.button_down + NativeInput.CLASSIC_BUTTON_LEFT -> R.string.button_left + NativeInput.CLASSIC_BUTTON_RIGHT -> R.string.button_right + NativeInput.CLASSIC_BUTTON_STICKL_UP -> R.string.button_stickl_up + NativeInput.CLASSIC_BUTTON_STICKL_DOWN -> R.string.button_stickl_down + NativeInput.CLASSIC_BUTTON_STICKL_LEFT -> R.string.button_stickl_left + NativeInput.CLASSIC_BUTTON_STICKL_RIGHT -> R.string.button_stickl_right + NativeInput.CLASSIC_BUTTON_STICKR_UP -> R.string.button_stickr_up + NativeInput.CLASSIC_BUTTON_STICKR_DOWN -> R.string.button_stickr_down + NativeInput.CLASSIC_BUTTON_STICKR_LEFT -> R.string.button_stickr_left + NativeInput.CLASSIC_BUTTON_STICKR_RIGHT -> R.string.button_stickr_right + else -> throw IllegalArgumentException("Invalid buttonId $buttonId for Classic controller type") +} + +@StringRes +fun vpadButtonToStringId(buttonId: Int) = when (buttonId) { + NativeInput.VPAD_BUTTON_A -> R.string.button_a + NativeInput.VPAD_BUTTON_B -> R.string.button_b + NativeInput.VPAD_BUTTON_X -> R.string.button_x + NativeInput.VPAD_BUTTON_Y -> R.string.button_y + NativeInput.VPAD_BUTTON_L -> R.string.button_l + NativeInput.VPAD_BUTTON_R -> R.string.button_r + NativeInput.VPAD_BUTTON_ZL -> R.string.button_zl + NativeInput.VPAD_BUTTON_ZR -> R.string.button_zr + NativeInput.VPAD_BUTTON_PLUS -> R.string.button_plus + NativeInput.VPAD_BUTTON_MINUS -> R.string.button_minus + NativeInput.VPAD_BUTTON_UP -> R.string.button_up + NativeInput.VPAD_BUTTON_DOWN -> R.string.button_down + NativeInput.VPAD_BUTTON_LEFT -> R.string.button_left + NativeInput.VPAD_BUTTON_RIGHT -> R.string.button_right + NativeInput.VPAD_BUTTON_STICKL -> R.string.button_stickl + NativeInput.VPAD_BUTTON_STICKR -> R.string.button_stickr + NativeInput.VPAD_BUTTON_STICKL_UP -> R.string.button_stickl_up + NativeInput.VPAD_BUTTON_STICKL_DOWN -> R.string.button_stickl_down + NativeInput.VPAD_BUTTON_STICKL_LEFT -> R.string.button_stickl_left + NativeInput.VPAD_BUTTON_STICKL_RIGHT -> R.string.button_stickl_right + NativeInput.VPAD_BUTTON_STICKR_UP -> R.string.button_stickr_up + NativeInput.VPAD_BUTTON_STICKR_DOWN -> R.string.button_stickr_down + NativeInput.VPAD_BUTTON_STICKR_LEFT -> R.string.button_stickr_left + NativeInput.VPAD_BUTTON_STICKR_RIGHT -> R.string.button_stickr_right + NativeInput.VPAD_BUTTON_MIC -> R.string.button_mic + NativeInput.VPAD_BUTTON_SCREEN -> R.string.button_screen + NativeInput.VPAD_BUTTON_HOME -> R.string.button_home + else -> throw IllegalArgumentException("Invalid buttonId $buttonId for VPAD controller type") +} + +@StringRes +fun wiimoteButtonItToStringId(buttonId: Int) = when (buttonId) { + NativeInput.WIIMOTE_BUTTON_A -> R.string.button_a + NativeInput.WIIMOTE_BUTTON_B -> R.string.button_b + NativeInput.WIIMOTE_BUTTON_1 -> R.string.button_1 + NativeInput.WIIMOTE_BUTTON_2 -> R.string.button_2 + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_Z -> R.string.button_nunchuck_z + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_C -> R.string.button_nunchuck_c + NativeInput.WIIMOTE_BUTTON_PLUS -> R.string.button_plus + NativeInput.WIIMOTE_BUTTON_MINUS -> R.string.button_minus + NativeInput.WIIMOTE_BUTTON_UP -> R.string.button_up + NativeInput.WIIMOTE_BUTTON_DOWN -> R.string.button_down + NativeInput.WIIMOTE_BUTTON_LEFT -> R.string.button_left + NativeInput.WIIMOTE_BUTTON_RIGHT -> R.string.button_right + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_UP -> R.string.button_nunchuck_up + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_DOWN -> R.string.button_nunchuck_down + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_LEFT -> R.string.button_nunchuck_left + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_RIGHT -> R.string.button_nunchuck_right + NativeInput.WIIMOTE_BUTTON_HOME -> R.string.button_home + else -> throw IllegalArgumentException("Invalid buttonId $buttonId for Wiimote controller type") +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/enumtostringmapper/native/SettingsEnums.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/enumtostringmapper/native/SettingsEnums.kt new file mode 100644 index 00000000..09e8613d --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/guicore/enumtostringmapper/native/SettingsEnums.kt @@ -0,0 +1,66 @@ +package info.cemu.cemu.guicore.enumtostringmapper.native + +import androidx.annotation.StringRes +import info.cemu.cemu.R +import info.cemu.cemu.nativeinterface.NativeSettings + +@StringRes +fun channelsToStringId(channels: Int) = when (channels) { + NativeSettings.AUDIO_CHANNELS_MONO -> R.string.mono + NativeSettings.AUDIO_CHANNELS_STEREO -> R.string.stereo + NativeSettings.AUDIO_CHANNELS_SURROUND -> R.string.surround + else -> throw IllegalArgumentException("Invalid channels type: $channels") +} + +@StringRes +fun consoleLanguageToStringId(channels: Int): Int = when (channels) { + NativeSettings.CONSOLE_LANGUAGE_JAPANESE -> R.string.console_language_japanese + NativeSettings.CONSOLE_LANGUAGE_ENGLISH -> R.string.console_language_english + NativeSettings.CONSOLE_LANGUAGE_FRENCH -> R.string.console_language_french + NativeSettings.CONSOLE_LANGUAGE_GERMAN -> R.string.console_language_german + NativeSettings.CONSOLE_LANGUAGE_ITALIAN -> R.string.console_language_italian + NativeSettings.CONSOLE_LANGUAGE_SPANISH -> R.string.console_language_spanish + NativeSettings.CONSOLE_LANGUAGE_CHINESE -> R.string.console_language_chinese + NativeSettings.CONSOLE_LANGUAGE_KOREAN -> R.string.console_language_korean + NativeSettings.CONSOLE_LANGUAGE_DUTCH -> R.string.console_language_dutch + NativeSettings.CONSOLE_LANGUAGE_PORTUGUESE -> R.string.console_language_portuguese + NativeSettings.CONSOLE_LANGUAGE_RUSSIAN -> R.string.console_language_russian + NativeSettings.CONSOLE_LANGUAGE_TAIWANESE -> R.string.console_language_taiwanese + else -> throw IllegalArgumentException("Invalid console language: $channels") +} + +@StringRes +fun vsyncModeToStringId(vsyncMode: Int) = when (vsyncMode) { + NativeSettings.VSYNC_MODE_OFF -> R.string.off + NativeSettings.VSYNC_MODE_DOUBLE_BUFFERING -> R.string.double_buffering + NativeSettings.VSYNC_MODE_TRIPLE_BUFFERING -> R.string.triple_buffering + else -> throw IllegalArgumentException("Invalid vsync mode: $vsyncMode") +} + +@StringRes +fun fullscreenScalingModeToStringId(fullscreenScaling: Int) = when (fullscreenScaling) { + NativeSettings.FULLSCREEN_SCALING_KEEP_ASPECT_RATIO -> R.string.keep_aspect_ratio + NativeSettings.FULLSCREEN_SCALING_STRETCH -> R.string.stretch + else -> throw IllegalArgumentException("Invalid fullscreen scaling mode: $fullscreenScaling") +} + +@StringRes +fun scalingFilterToStringId(scalingFilter: Int) = when (scalingFilter) { + NativeSettings.SCALING_FILTER_BILINEAR_FILTER -> R.string.bilinear + NativeSettings.SCALING_FILTER_BICUBIC_FILTER -> R.string.bicubic + NativeSettings.SCALING_FILTER_BICUBIC_HERMITE_FILTER -> R.string.hermite + NativeSettings.SCALING_FILTER_NEAREST_NEIGHBOR_FILTER -> R.string.nearest_neighbor + else -> throw IllegalArgumentException("Invalid scaling filter: $scalingFilter") +} + +@StringRes +fun overlayScreenPositionToStringId(overlayScreenPosition: Int) = when (overlayScreenPosition) { + NativeSettings.OVERLAY_SCREEN_POSITION_DISABLED -> R.string.overlay_position_disabled + NativeSettings.OVERLAY_SCREEN_POSITION_TOP_LEFT -> R.string.overlay_position_top_left + NativeSettings.OVERLAY_SCREEN_POSITION_TOP_CENTER -> R.string.overlay_position_top_center + NativeSettings.OVERLAY_SCREEN_POSITION_TOP_RIGHT -> R.string.overlay_position_top_right + NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_LEFT -> R.string.overlay_position_bottom_left + NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_CENTER -> R.string.overlay_position_bottom_center + NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_RIGHT -> R.string.overlay_position_bottom_right + else -> throw IllegalArgumentException("Invalid overlay position: $overlayScreenPosition") +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/theme/Theme.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/theme/Theme.kt new file mode 100644 index 00000000..4f033aa0 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/guicore/theme/Theme.kt @@ -0,0 +1,37 @@ +package info.cemu.cemu.guicore.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme() + +private val LightColorScheme = lightColorScheme() + +@Composable +fun CemuTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + content: @Composable () -> Unit, +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + content = content + ) +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/inputoverlay/InputOverlaySettingsFragment.kt b/src/android/app/src/main/java/info/cemu/cemu/inputoverlay/InputOverlaySettingsFragment.kt deleted file mode 100644 index e81318b5..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/inputoverlay/InputOverlaySettingsFragment.kt +++ /dev/null @@ -1,90 +0,0 @@ -package info.cemu.cemu.inputoverlay - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import info.cemu.cemu.R -import info.cemu.cemu.databinding.LayoutGenericRecyclerViewBinding -import info.cemu.cemu.guibasecomponents.GenericRecyclerViewAdapter -import info.cemu.cemu.guibasecomponents.SingleSelectionRecyclerViewItem -import info.cemu.cemu.guibasecomponents.SliderRecyclerViewItem -import info.cemu.cemu.guibasecomponents.ToggleRecyclerViewItem -import info.cemu.cemu.inputoverlay.InputOverlaySettingsProvider.OverlaySettings -import info.cemu.cemu.nativeinterface.NativeInput -import java.util.stream.Collectors -import java.util.stream.IntStream - - -class InputOverlaySettingsFragment : Fragment() { - private var overlaySettings: OverlaySettings? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (overlaySettings == null) { - val inputOverlaySettingsProvider = InputOverlaySettingsProvider(requireContext()) - overlaySettings = inputOverlaySettingsProvider.overlaySettings - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val binding = LayoutGenericRecyclerViewBinding.inflate(inflater, container, false) - val genericRecyclerViewAdapter = GenericRecyclerViewAdapter() - - val inputOverlayToggle = ToggleRecyclerViewItem( - getString(R.string.input_overlay), - getString(R.string.enable_input_overlay), - overlaySettings!!.isOverlayEnabled - ) { checked: Boolean -> - overlaySettings!!.isOverlayEnabled = checked - overlaySettings!!.saveSettings() - } - genericRecyclerViewAdapter.addRecyclerViewItem(inputOverlayToggle) - - val vibrateOnTouchToggle = ToggleRecyclerViewItem( - getString(R.string.vibrate), - getString(R.string.enable_vibrate_on_touch), - overlaySettings!!.isVibrateOnTouchEnabled - ) { checked: Boolean -> - overlaySettings!!.isVibrateOnTouchEnabled = checked - overlaySettings!!.saveSettings() - } - genericRecyclerViewAdapter.addRecyclerViewItem(vibrateOnTouchToggle) - - val alphaSlider = SliderRecyclerViewItem( - getString(R.string.alpha_slider), - 0f, - 255f, - overlaySettings!!.alpha.toFloat() - ) { value: Float -> - overlaySettings!!.alpha = value.toInt() - overlaySettings!!.saveSettings() - } - genericRecyclerViewAdapter.addRecyclerViewItem(alphaSlider) - - val controllerSelection = SingleSelectionRecyclerViewItem( - getString(R.string.overlay_controller), - overlaySettings!!.controllerIndex, - IntStream.range(0, NativeInput.MAX_CONTROLLERS).boxed().collect(Collectors.toList()), - { controllerIndex: Int -> - getString( - R.string.controller_numbered, - controllerIndex + 1 - ) - }, - { controllerIndex: Int? -> - overlaySettings!!.controllerIndex = - controllerIndex!! - }) - genericRecyclerViewAdapter.addRecyclerViewItem(controllerSelection) - - binding.recyclerView.adapter = genericRecyclerViewAdapter - - return binding.root - } -} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/inputoverlay/InputOverlaySettingsProvider.kt b/src/android/app/src/main/java/info/cemu/cemu/inputoverlay/InputOverlaySettingsProvider.kt index 6db90dfa..bc5314b4 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/inputoverlay/InputOverlaySettingsProvider.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/inputoverlay/InputOverlaySettingsProvider.kt @@ -3,15 +3,64 @@ package info.cemu.cemu.inputoverlay import android.content.Context import android.content.SharedPreferences import android.graphics.Rect +import androidx.annotation.IntRange +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.MutablePreferences +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import info.cemu.cemu.CemuApplication import info.cemu.cemu.nativeinterface.NativeInput +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import java.util.Optional -import java.util.function.Consumer -import kotlin.math.max -import kotlin.math.min +private const val INPUT_DATA_STORE_NAME = "input-settings22" +private val Context.dataStore: DataStore by preferencesDataStore(name = INPUT_DATA_STORE_NAME) +private fun Preferences.get(key: Preferences.Key, default: T) = + this[key] ?: default + +class OverlaySettings( + var isVibrateOnTouchEnabled: Boolean, + var isOverlayEnabled: Boolean, + @IntRange(0, (NativeInput.MAX_CONTROLLERS - 1L)) + var controllerIndex: Int, + @IntRange(0, 255) + var alpha: Int, +) { + companion object { + private const val DEFAULT_ALPHA: Int = 64 + private const val IS_VIBRATE_ON_TOUCH_ENABLED_KEY = "IS_VIBRATE_ON_TOUCH_ENABLED" + private const val IS_OVERLAY_ENABLED_KEY = "IS_OVERLAY_ENABLED" + private const val CONTROLLER_INDEX_KEY = "CONTROLLER_INDEX" + private const val ALPHA_KEY = "ALPHA" + } + + internal constructor(sharedPreferences: SharedPreferences) : this( + isVibrateOnTouchEnabled = sharedPreferences.getBoolean( + IS_VIBRATE_ON_TOUCH_ENABLED_KEY, + false + ), + isOverlayEnabled = sharedPreferences.getBoolean(IS_OVERLAY_ENABLED_KEY, false), + controllerIndex = sharedPreferences.getInt(CONTROLLER_INDEX_KEY, 0), + alpha = sharedPreferences.getInt(ALPHA_KEY, DEFAULT_ALPHA), + ) + + internal fun save(sharedPreferences: SharedPreferences) { + sharedPreferences.edit().apply { + putBoolean(IS_VIBRATE_ON_TOUCH_ENABLED_KEY, isVibrateOnTouchEnabled) + putBoolean(IS_OVERLAY_ENABLED_KEY, isOverlayEnabled) + putInt(CONTROLLER_INDEX_KEY, controllerIndex) + putInt(ALPHA_KEY, alpha) + apply() + } + } +} class InputOverlaySettingsProvider(context: Context) { - enum class Input { + enum class OverlayInput { A, B, ONE, @@ -35,80 +84,51 @@ class InputOverlaySettingsProvider(context: Context) { RIGHT_AXIS, } - private val sharedPreferences: SharedPreferences + val sharedPreferences: SharedPreferences = + context.getSharedPreferences("input-overlay-settings", Context.MODE_PRIVATE); - init { - this.sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - } + var overlaySettings: OverlaySettings + get() = OverlaySettings(sharedPreferences) + set(value) = value.save(sharedPreferences) - val overlaySettings: OverlaySettings - get() = OverlaySettings( - isVibrateOnTouchEnabled, - isOverlayEnabled, - controllerIndex, - alpha, - { settings: OverlaySettings -> - val editor: SharedPreferences.Editor = sharedPreferences.edit() - editor.putInt("controllerIndex", settings.controllerIndex) - editor.putInt("alpha", settings.alpha) - editor.putBoolean("overlayEnabled", settings.isOverlayEnabled) - editor.putBoolean("vibrateOnTouchEnabled", settings.isVibrateOnTouchEnabled) - editor.apply() - } - ) - fun getOverlaySettingsForInput(input: Input, width: Int, height: Int): InputOverlaySettings { +// private val sharedPreferences: SharedPreferences = +// context.getSharedPreferences("settings", Context.MODE_PRIVATE) + + fun getOverlaySettingsForInput( + input: OverlayInput, + width: Int, + height: Int + ): InputOverlaySettings { return InputOverlaySettings( getRectangle(input).orElseGet({ getDefaultRectangle(input, width, height) }), - alpha, + 100, { settings: InputOverlaySettings -> - val editor: SharedPreferences.Editor = sharedPreferences.edit() - val rect: Rect? = settings.rect - editor.putInt(input.toString() + "left", rect!!.left) - editor.putInt(input.toString() + "top", rect.top) - editor.putInt(input.toString() + "right", rect.right) - editor.putInt(input.toString() + "bottom", rect.bottom) - editor.putInt(input.toString() + "alpha", settings.alpha) - editor.apply() +// val editor: SharedPreferences.Editor = sharedPreferences.edit() +// val rect: Rect? = settings.rect +// editor.putInt(input.toString() + "left", rect!!.left) +// editor.putInt(input.toString() + "top", rect.top) +// editor.putInt(input.toString() + "right", rect.right) +// editor.putInt(input.toString() + "bottom", rect.bottom) +// editor.putInt(input.toString() + "alpha", settings.alpha) +// editor.apply() } ) } - private val isOverlayEnabled: Boolean - get() { - return sharedPreferences.getBoolean("overlayEnabled", true) - } - private val isVibrateOnTouchEnabled: Boolean - get() { - return sharedPreferences.getBoolean("vibrateOnTouchEnabled", true) - } - - private val controllerIndex: Int - get() { - return sharedPreferences.getInt("controllerIndex", 0) - } - - private val alpha: Int - get() { - return sharedPreferences.getInt( - "alpha", - DEFAULT_ALPHA - ) - } - - private fun getRectangle(input: Input): Optional { - val left: Int = sharedPreferences.getInt(input.toString() + "left", -1) - val top: Int = sharedPreferences.getInt(input.toString() + "top", -1) - val right: Int = sharedPreferences.getInt(input.toString() + "right", -1) - val bottom: Int = sharedPreferences.getInt(input.toString() + "bottom", -1) - if (left == -1 || top == -1 || right == -1 || bottom == -1) { - return Optional.empty() - } - return Optional.of(Rect(left, top, right, bottom)) + private fun getRectangle(input: OverlayInput): Optional { +// val left: Int = sharedPreferences.getInt(input.toString() + "left", -1) +// val top: Int = sharedPreferences.getInt(input.toString() + "top", -1) +// val right: Int = sharedPreferences.getInt(input.toString() + "right", -1) +// val bottom: Int = sharedPreferences.getInt(input.toString() + "bottom", -1) +// if (left == -1 || top == -1 || right == -1 || bottom == -1) { +// } + return Optional.empty() +// return Optional.of(Rect(left, top, right, bottom)) } - fun getDefaultRectangle(input: Input, width: Int, height: Int): Rect { + fun getDefaultRectangle(input: OverlayInput, width: Int, height: Int): Rect { val pmButtonRadius: Int = (height * 0.065f).toInt() val pmButtonsCentreX: Int = (width * 0.5f).toInt() val pmButtonsCentreY: Int = (height * 0.875f).toInt() @@ -135,106 +155,106 @@ class InputOverlaySettingsProvider(context: Context) { val joystickClickRadius: Int = (joystickRadius * 0.7f).toInt() // TODO: move this to res? return when (input) { - Input.A -> Rect( + OverlayInput.A -> Rect( abxyButtonsCentreX + roundButtonRadius, abxyButtonsCentreY - roundButtonRadius, abxyButtonsCentreX + roundButtonRadius * 3, abxyButtonsCentreY + roundButtonRadius ) - Input.B -> Rect( + OverlayInput.B -> Rect( abxyButtonsCentreX - roundButtonRadius, abxyButtonsCentreY + roundButtonRadius, abxyButtonsCentreX + roundButtonRadius, abxyButtonsCentreY + roundButtonRadius * 3 ) - Input.X, Input.ONE -> Rect( + OverlayInput.X, OverlayInput.ONE -> Rect( abxyButtonsCentreX - roundButtonRadius, abxyButtonsCentreY - roundButtonRadius * 3, abxyButtonsCentreX + roundButtonRadius, abxyButtonsCentreY - roundButtonRadius ) - Input.Y, Input.TWO -> Rect( + OverlayInput.Y, OverlayInput.TWO -> Rect( abxyButtonsCentreX - roundButtonRadius * 3, abxyButtonsCentreY - roundButtonRadius, abxyButtonsCentreX - roundButtonRadius, abxyButtonsCentreY + roundButtonRadius ) - Input.MINUS -> Rect( + OverlayInput.MINUS -> Rect( pmButtonsCentreX - pmButtonRadius * 3, pmButtonsCentreY - pmButtonRadius, pmButtonsCentreX - pmButtonRadius, pmButtonsCentreY + pmButtonRadius ) - Input.PLUS -> Rect( + OverlayInput.PLUS -> Rect( pmButtonsCentreX + pmButtonRadius, pmButtonsCentreY - pmButtonRadius, pmButtonsCentreX + pmButtonRadius * 3, pmButtonsCentreY + pmButtonRadius ) - Input.L -> Rect( + OverlayInput.L -> Rect( triggersLLeft, triggersTop, triggersLLeft + rectangleButtonsWidth, triggersTop + rectangleButtonsHeight ) - Input.ZL -> Rect( + OverlayInput.ZL -> Rect( triggersLLeft, (triggersTop + rectangleButtonsHeight * 1.5f).toInt(), triggersLLeft + rectangleButtonsWidth, (triggersTop + rectangleButtonsHeight * 2.5f).toInt() ) - Input.R -> Rect( + OverlayInput.R -> Rect( triggersRLeft, triggersTop, triggersRLeft + rectangleButtonsWidth, triggersTop + rectangleButtonsHeight ) - Input.ZR -> Rect( + OverlayInput.ZR -> Rect( triggersRLeft, (triggersTop + rectangleButtonsHeight * 1.5f).toInt(), triggersRLeft + rectangleButtonsWidth, (triggersTop + rectangleButtonsHeight * 2.5f).toInt() ) - Input.C, Input.HOME, Input.Z, Input.NUN_CHUCK_AXIS -> Rect() - Input.DPAD -> Rect( + OverlayInput.C, OverlayInput.HOME, OverlayInput.Z, OverlayInput.NUN_CHUCK_AXIS -> Rect() + OverlayInput.DPAD -> Rect( dpadCentreX - dpadRadius, dpadCentreY - dpadRadius, dpadCentreX + dpadRadius, dpadCentreY + dpadRadius ) - Input.LEFT_AXIS -> Rect( + OverlayInput.LEFT_AXIS -> Rect( leftJoystickCentreX - joystickRadius, joystickCentreY - joystickRadius, leftJoystickCentreX + joystickRadius, joystickCentreY + joystickRadius ) - Input.RIGHT_AXIS -> Rect( + OverlayInput.RIGHT_AXIS -> Rect( rightJoystickCentreX - joystickRadius, joystickCentreY - joystickRadius, rightJoystickCentreX + joystickRadius, joystickCentreY + joystickRadius ) - Input.L_STICK -> Rect( + OverlayInput.L_STICK -> Rect( (leftJoystickCentreX + joystickRadius * 1.5f - joystickClickRadius).toInt(), (joystickCentreY + joystickRadius * 1.5f - joystickClickRadius).toInt(), (leftJoystickCentreX + joystickRadius * 1.5f + joystickClickRadius).toInt(), (joystickCentreY + joystickRadius * 1.5f + joystickClickRadius).toInt() ) - Input.R_STICK -> Rect( + OverlayInput.R_STICK -> Rect( (rightJoystickCentreX - joystickRadius * 1.5f - joystickClickRadius).toInt(), (joystickCentreY + joystickRadius * 1.5f - joystickClickRadius).toInt(), (rightJoystickCentreX - joystickRadius * 1.5f + joystickClickRadius).toInt(), @@ -243,33 +263,34 @@ class InputOverlaySettingsProvider(context: Context) { } } - class OverlaySettings internal constructor( - var isVibrateOnTouchEnabled: Boolean, - var isOverlayEnabled: Boolean, - controllerIndex: Int, - alpha: Int, - private val overlaySettingsConsumer: (OverlaySettings) -> Unit - ) { - var controllerIndex: Int = 0 - set(controllerIndex) { - require(controllerIndex in 0.. Unit +// ) { +// var controllerIndex: Int = 0 +// set(controllerIndex) { +// require(controllerIndex in 0..? = null private val settingsProvider: InputOverlaySettingsProvider private val vibrator: Vibrator? private val buttonTouchVibrationEffect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK) - private val vibrateOnTouch: Boolean - private val inputsMinWidthHeight: Int + private var vibrateOnTouch: Boolean = false + private var inputsMinWidthHeight: Int = 24 private fun getVibrator(context: Context): Vibrator { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { @@ -113,6 +112,7 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) : context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager return vibratorManager.defaultVibrator } + @Suppress("DEPRECATION") return context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator } @@ -311,7 +311,7 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) : onJoystickChange(joystick, up, down, left, right) } - private fun getOverlaySettingsForInput(input: InputOverlaySettingsProvider.Input): InputOverlaySettings { + private fun getOverlaySettingsForInput(input: InputOverlaySettingsProvider.OverlayInput): InputOverlaySettings { return settingsProvider.getOverlaySettingsForInput(input, width, height) } @@ -334,21 +334,21 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) : R.drawable.button_minus, ::onButtonStateChange, OverlayButton.MINUS, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.MINUS) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.MINUS) ), RoundButton( resources, R.drawable.button_plus, ::onButtonStateChange, OverlayButton.PLUS, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.PLUS) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.PLUS) ), DPadInput( resources, R.drawable.dpad_background, R.drawable.dpad_button, ::onButtonStateChange, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.DPAD) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.DPAD) ), Joystick( resources, @@ -356,21 +356,21 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) : R.drawable.stick_inner, ::onJoystickStateChange, OverlayJoystick.LEFT, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.LEFT_AXIS) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.LEFT_AXIS) ), RoundButton( resources, R.drawable.button_a, ::onButtonStateChange, OverlayButton.A, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.A) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.A) ), RoundButton( resources, R.drawable.button_b, ::onButtonStateChange, OverlayButton.B, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.B) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.B) ) ).apply { if (nativeControllerType != NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE) { @@ -380,42 +380,42 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) : R.drawable.button_x, ::onButtonStateChange, OverlayButton.X, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.X) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.X) ), RoundButton( resources, R.drawable.button_y, ::onButtonStateChange, OverlayButton.Y, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.Y) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.Y) ), RectangleButton( resources, R.drawable.button_zl, ::onButtonStateChange, OverlayButton.ZL, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.ZL) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.ZL) ), RectangleButton( resources, R.drawable.button_l, ::onButtonStateChange, OverlayButton.L, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.L) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.L) ), RectangleButton( resources, R.drawable.button_zr, ::onButtonStateChange, OverlayButton.ZR, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.ZR) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.ZR) ), RectangleButton( resources, R.drawable.button_r, ::onButtonStateChange, OverlayButton.R, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.R) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.R) ), Joystick( resources, @@ -423,7 +423,7 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) : R.drawable.stick_inner, ::onJoystickStateChange, OverlayJoystick.RIGHT, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.RIGHT_AXIS) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.RIGHT_AXIS) ) ) } @@ -434,21 +434,21 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) : R.drawable.button_c, ::onButtonStateChange, OverlayButton.C, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.C) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.C) ), RoundButton( resources, R.drawable.button_z, ::onButtonStateChange, OverlayButton.Z, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.Z) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.Z) ), RoundButton( resources, R.drawable.button_home, ::onButtonStateChange, OverlayButton.HOME, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.HOME) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.HOME) ) ) } @@ -461,14 +461,14 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) : R.drawable.button_stick, ::onButtonStateChange, OverlayButton.L_STICK, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.L_STICK) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.L_STICK) ), RoundButton( resources, R.drawable.button_stick, ::onButtonStateChange, OverlayButton.R_STICK, - getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.R_STICK) + getOverlaySettingsForInput(InputOverlaySettingsProvider.OverlayInput.R_STICK) ) ) } diff --git a/src/android/app/src/main/java/info/cemu/cemu/io/Files.kt b/src/android/app/src/main/java/info/cemu/cemu/io/Files.kt deleted file mode 100644 index 4f29ed8b..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/io/Files.kt +++ /dev/null @@ -1,18 +0,0 @@ -package info.cemu.cemu.io - -import java.io.File -import java.io.IOException - -object Files { - @JvmStatic - @Throws(IOException::class) - fun delete(file: File) { - val files = file.listFiles() - if (files == null) { - file.delete() - return - } - files.forEach { delete(it) } - file.delete() - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeActiveSettings.kt b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeActiveSettings.kt new file mode 100644 index 00000000..562b4267 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeActiveSettings.kt @@ -0,0 +1,6 @@ +package info.cemu.cemu.nativeinterface + +object NativeActiveSettings { + @JvmStatic + external fun getMLCPath(): String +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/FileCallbacks.kt b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeFiles.kt similarity index 69% rename from src/android/app/src/main/java/info/cemu/cemu/nativeinterface/FileCallbacks.kt rename to src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeFiles.kt index e04abbfd..a010f733 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/FileCallbacks.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeFiles.kt @@ -5,17 +5,18 @@ package info.cemu.cemu.nativeinterface import android.net.Uri import android.provider.DocumentsContract import android.util.Log +import androidx.annotation.Keep import info.cemu.cemu.CemuApplication -import info.cemu.cemu.io.Files -import java.io.IOException -import java.nio.file.Path private const val PATH_SEPARATOR_ENCODED = "%2F" private const val PATH_SEPARATOR_DECODED = "/" private const val COLON_ENCODED = "%3A" private const val MODE = "r" -private fun Uri.toCppPath(): String { +private val ContentResolver + get() = CemuApplication.Application.applicationContext.contentResolver + +fun Uri.toNativePath(): String { val uriPath = toString() val delimiterPos = uriPath.lastIndexOf(COLON_ENCODED) if (delimiterPos == -1) { @@ -26,7 +27,7 @@ private fun Uri.toCppPath(): String { ) } -private fun String.fromCppPath(): Uri { +fun String.fromNativePath(): Uri { val delimiterPos = lastIndexOf(COLON_ENCODED) if (delimiterPos == -1) { return Uri.parse(this) @@ -38,14 +39,15 @@ private fun String.fromCppPath(): Uri { ) } +@Keep fun openContentUri(uri: String): Int { try { if (!exists(uri)) { return -1 } val parcelFileDescriptor = - CemuApplication.application.applicationContext.contentResolver.openFileDescriptor( - uri.fromCppPath(), MODE + ContentResolver.openFileDescriptor( + uri.fromNativePath(), MODE ) if (parcelFileDescriptor != null) { val fd = parcelFileDescriptor.detachFd() @@ -53,20 +55,21 @@ fun openContentUri(uri: String): Int { return fd } } catch (e: Exception) { - Log.e("FileCallbacks", "Cannot open content uri, error: " + e.message) + Log.e("NativeFiles", "Cannot open content uri, error: ${e.message}") } return -1 } +@Keep fun listFiles(uri: String): Array { val files = ArrayList() - val directoryUri = uri.fromCppPath() + val directoryUri = uri.fromNativePath() val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( directoryUri, DocumentsContract.getDocumentId(directoryUri) ) try { - CemuApplication.application.applicationContext.contentResolver.query( + ContentResolver.query( childrenUri, arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID), null, @@ -77,44 +80,38 @@ fun listFiles(uri: String): Array { val documentId = cursor.getString(0) val documentUri = DocumentsContract.buildDocumentUriUsingTree(directoryUri, documentId) - files.add(documentUri.toCppPath()) + files.add(documentUri.toNativePath()) } } } catch (e: Exception) { - Log.e("FileCallbacks", "Cannot list files: " + e.message) + Log.e("NativeFiles", "Cannot list files: ${e.message}") } var filesArray = arrayOfNulls(files.size) filesArray = files.toArray(filesArray) return filesArray } +@Keep fun isDirectory(uri: String): Boolean { - val mimeType = CemuApplication.application.applicationContext.contentResolver.getType( - uri.fromCppPath() + val mimeType = ContentResolver.getType( + uri.fromNativePath() ) return DocumentsContract.Document.MIME_TYPE_DIR == mimeType } +@Keep fun isFile(uri: String): Boolean { return !isDirectory(uri) } +@Keep fun exists(uri: String): Boolean { try { - CemuApplication.application.applicationContext.contentResolver.query( - uri.fromCppPath(), null, null, null, null - ).use { cursor -> + ContentResolver.query(uri.fromNativePath(), null, null, null, null).use { cursor -> return cursor != null && cursor.moveToFirst() } } catch (e: Exception) { - Log.e("FileCallbacks", "Failed checking if file exists: " + e.message) + Log.e("NativeFiles", "Failed checking if file exists: ${e.message}") return false } } - -fun delete(fileToDelete: Path) { - try { - Files.delete(fileToDelete.toFile()) - } catch (ignored: IOException) { - } -} 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 3252a07d..9439c552 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 @@ -1,6 +1,9 @@ package info.cemu.cemu.nativeinterface import android.graphics.Bitmap +import androidx.annotation.Keep +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap object NativeGameTitles { const val CONSOLE_REGION_JPN: Int = 0x1 @@ -29,7 +32,6 @@ object NativeGameTitles { @JvmStatic external fun setCpuModeForTitle(gameTitleId: Long, cpuMode: Int) - @JvmField val THREAD_QUANTUM_VALUES: IntArray = intArrayOf( 20000, 45000, @@ -66,23 +68,22 @@ object NativeGameTitles { external fun reloadGameTitles() @JvmStatic - val installedGamesTitleIds: ArrayList? - external get + external fun getInstalledGamesTitleIds(): List - @JvmRecord + @Keep data class Game( - @JvmField val titleId: Long, - @JvmField val path: String?, - @JvmField val name: String?, - @JvmField val version: Short, - @JvmField val dlc: Short, - @JvmField val region: Int, - @JvmField val lastPlayedYear: Short, - @JvmField val lastPlayedMonth: Short, - @JvmField val lastPlayedDay: Short, - @JvmField val minutesPlayed: Int, - @JvmField val isFavorite: Boolean, - @JvmField val icon: Bitmap? + val titleId: Long, + val path: String?, + val name: String?, + val version: Short, + val dlc: Short, + val region: Int, + val lastPlayedYear: Short, + val lastPlayedMonth: Short, + val lastPlayedDay: Short, + val minutesPlayed: Int, + val isFavorite: Boolean, + private val _icon: Bitmap?, ) : Comparable { override fun compareTo(other: Game): Int { if (titleId == other.titleId) { @@ -99,8 +100,11 @@ object NativeGameTitles { } return name?.compareTo(other.name ?: "") ?: 0 } + + val icon: ImageBitmap? = _icon?.asImageBitmap() } + @Keep fun interface GameTitleLoadedCallback { fun onGameTitleLoaded(game: Game?) } diff --git a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeGraphicPacks.kt b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeGraphicPacks.kt index 0b94a119..eb2d7255 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeGraphicPacks.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeGraphicPacks.kt @@ -1,11 +1,11 @@ package info.cemu.cemu.nativeinterface +import androidx.annotation.Keep import java.util.Objects object NativeGraphicPacks { @JvmStatic - val graphicPackBasicInfos: ArrayList? - external get + external fun getGraphicPackBasicInfos(): List @JvmStatic external fun refreshGraphicPacks() @@ -22,22 +22,23 @@ object NativeGraphicPacks { @JvmStatic external fun getGraphicPackPresets(id: Long): ArrayList - @JvmRecord + @Keep data class GraphicPackBasicInfo( - @JvmField val id: Long, - @JvmField val virtualPath: String, - @JvmField val enabled: Boolean, - @JvmField val titleIds: ArrayList + val id: Long, + val virtualPath: String, + val enabled: Boolean, + val titleIds: ArrayList ) + @Keep class GraphicPackPreset( private val graphicPackId: Long, - @JvmField val category: String?, - @JvmField val presets: ArrayList, - private var activePreset: String + val category: String?, + val presets: ArrayList, + private var _activePreset: String ) { override fun hashCode(): Int { - return Objects.hash(graphicPackId, category, presets, activePreset) + return Objects.hash(graphicPackId, category, presets, _activePreset) } override fun equals(other: Any?): Boolean { @@ -53,33 +54,32 @@ object NativeGraphicPacks { return false } - fun getActivePreset(): String { - return activePreset - } - - fun setActivePreset(activePreset: String) { - require( - !presets.stream() - .noneMatch { s: String -> s == activePreset }) { "Trying to set an invalid preset: $activePreset" } - setGraphicPackActivePreset(graphicPackId, category, activePreset) - this.activePreset = activePreset - } + var activePreset: String + get() = _activePreset + set(value) { + require(presets.any { it == value }) { "Trying to set an invalid preset: $value" } + setGraphicPackActivePreset(graphicPackId, category, value) + _activePreset = value + } } + @Keep class GraphicPack( - @JvmField val id: Long, + val id: Long, private var active: Boolean, - @JvmField val name: String, - @JvmField val description: String, - private var presets: ArrayList + val name: String, + val description: String, + private var _presets: ArrayList ) { fun isActive(): Boolean { return active } - fun getPresets(): List = presets + val presets: List + get() = _presets + fun reloadPresets() { - presets = getGraphicPackPresets(id) + _presets = getGraphicPackPresets(id) } fun setActive(active: Boolean) { diff --git a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeInput.kt b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeInput.kt index 03703780..78aa2d62 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeInput.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeInput.kt @@ -182,10 +182,10 @@ object NativeInput { external fun clearControllerMapping(index: Int, mappingId: Int) @JvmStatic - external fun getControllerMapping(index: Int, mappingId: Int): String? + external fun getControllerMapping(index: Int, mappingId: Int): String @JvmStatic - external fun getControllerMappings(index: Int): Map? + external fun getControllerMappings(index: Int): Map @JvmStatic external fun onTouchDown(x: Int, y: Int, isTV: Boolean) diff --git a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeSettings.kt b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeSettings.kt index 6ef9890a..f46f9cdf 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeSettings.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeSettings.kt @@ -8,30 +8,32 @@ object NativeSettings { external fun removeGamesPath(uri: String?) @JvmStatic - val gamesPaths: ArrayList - external get + external fun getGamesPaths(): ArrayList @JvmStatic - var asyncShaderCompile: Boolean - external get - external set + external fun getAsyncShaderCompile(): Boolean + + @JvmStatic + external fun setAsyncShaderCompile(value: Boolean) const val VSYNC_MODE_OFF: Int = 0 const val VSYNC_MODE_DOUBLE_BUFFERING: Int = 1 const val VSYNC_MODE_TRIPLE_BUFFERING: Int = 2 @JvmStatic - var vSyncMode: Int - external get - external set + external fun getVsyncMode(): Int + + @JvmStatic + external fun setVsyncMode(value: Int) const val FULLSCREEN_SCALING_KEEP_ASPECT_RATIO: Int = 0 const val FULLSCREEN_SCALING_STRETCH: Int = 1 @JvmStatic - var fullscreenScaling: Int - external get - external set + external fun getFullscreenScaling(): Int + + @JvmStatic + external fun setFullscreenScaling(value: Int) const val SCALING_FILTER_BILINEAR_FILTER: Int = 0 const val SCALING_FILTER_BICUBIC_FILTER: Int = 1 @@ -39,19 +41,22 @@ object NativeSettings { const val SCALING_FILTER_NEAREST_NEIGHBOR_FILTER: Int = 3 @JvmStatic - var upscalingFilter: Int - external get - external set + external fun getUpscalingFilter(): Int @JvmStatic - var downscalingFilter: Int - external get - external set + external fun setUpscalingFilter(value: Int) @JvmStatic - var accurateBarriers: Boolean - external get - external set + external fun getDownscalingFilter(): Int + + @JvmStatic + external fun setDownscalingFilter(value: Int) + + @JvmStatic + external fun getAccurateBarriers(): Boolean + + @JvmStatic + external fun setAccurateBarriers(value: Boolean) @JvmStatic external fun getAudioDeviceEnabled(tv: Boolean): Boolean @@ -78,12 +83,13 @@ object NativeSettings { @JvmStatic external fun getAudioDeviceVolume(tv: Boolean): Int - const val AUDIO_BLOCK_COUNT: Int = 24 + const val AUDIO_LATENCY_MS_MAX: Int = 276 @JvmStatic - var audioLatency: Int - external get - external set + external fun getAudioLatency(): Int + + @JvmStatic + external fun setAudioLatency(value: Int) const val OVERLAY_SCREEN_POSITION_DISABLED: Int = 0 const val OVERLAY_SCREEN_POSITION_TOP_LEFT: Int = 1 @@ -94,72 +100,79 @@ object NativeSettings { const val OVERLAY_SCREEN_POSITION_BOTTOM_RIGHT: Int = 6 @JvmStatic - var overlayPosition: Int - external get - external set + external fun getOverlayPosition(): Int + + @JvmStatic + external fun setOverlayPosition(value: Int) const val OVERLAY_TEXT_SCALE_MIN: Int = 50 const val OVERLAY_TEXT_SCALE_MAX: Int = 300 @JvmStatic - var overlayTextScalePercentage: Int - external get - external set - + external fun getOverlayTextScalePercentage(): Int @JvmStatic - var isOverlayFPSEnabled: Boolean - external get - external set + external fun setOverlayTextScalePercentage(value: Int) @JvmStatic - var isOverlayDrawCallsPerFrameEnabled: Boolean - external get - external set + external fun isOverlayFPSEnabled(): Boolean @JvmStatic - var isOverlayCPUUsageEnabled: Boolean - external get - external set - - var isOverlayCPUPerCoreUsageEnabled: Boolean - external get - external set + external fun setOverlayFPSEnabled(value: Boolean) @JvmStatic - var isOverlayRAMUsageEnabled: Boolean - external get - external set + external fun isOverlayDrawCallsPerFrameEnabled(): Boolean @JvmStatic - var isOverlayDebugEnabled: Boolean - external get - external set + external fun setOverlayDrawCallsPerFrameEnabled(value: Boolean) @JvmStatic - var notificationsPosition: Int - external get - external set + external fun isOverlayCPUUsageEnabled(): Boolean @JvmStatic - var notificationsTextScalePercentage: Int - external get - external set + external fun setOverlayCPUUsageEnabled(value: Boolean) @JvmStatic - var isNotificationControllerProfilesEnabled: Boolean - external get - external set + external fun isOverlayRAMUsageEnabled(): Boolean @JvmStatic - var isNotificationShaderCompilerEnabled: Boolean - external get - external set + external fun setOverlayRAMUsageEnabled(value: Boolean) @JvmStatic - var isNotificationFriendListEnabled: Boolean - external get - external set + external fun isOverlayDebugEnabled(): Boolean + + @JvmStatic + external fun setOverlayDebugEnabled(value: Boolean) + + @JvmStatic + external fun getNotificationsPosition(): Int + + @JvmStatic + external fun setNotificationsPosition(value: Int) + + @JvmStatic + external fun getNotificationsTextScalePercentage(): Int + + @JvmStatic + external fun setNotificationsTextScalePercentage(value: Int) + + @JvmStatic + external fun isNotificationControllerProfilesEnabled(): Boolean + + @JvmStatic + external fun setNotificationControllerProfilesEnabled(value: Boolean) + + @JvmStatic + external fun isNotificationShaderCompilerEnabled(): Boolean + + @JvmStatic + external fun setNotificationShaderCompilerEnabled(value: Boolean) + + @JvmStatic + external fun isNotificationFriendListEnabled(): Boolean + + @JvmStatic + external fun setNotificationFriendListEnabled(value: Boolean) const val CONSOLE_LANGUAGE_JAPANESE: Int = 0 const val CONSOLE_LANGUAGE_ENGLISH: Int = 1 @@ -175,7 +188,8 @@ object NativeSettings { const val CONSOLE_LANGUAGE_TAIWANESE: Int = 11 @JvmStatic - var consoleLanguage: Int - external get - external set + external fun getConsoleLanguage(): Int + + @JvmStatic + external fun setConsoleLanguage(value: Int) } diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsActivity.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsActivity.kt index dae82537..2f7f868d 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsActivity.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsActivity.kt @@ -1,30 +1,160 @@ package info.cemu.cemu.settings import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import androidx.navigation.fragment.NavHostFragment -import androidx.navigation.ui.AppBarConfiguration -import androidx.navigation.ui.AppBarConfiguration.Builder -import androidx.navigation.ui.AppBarConfiguration.OnNavigateUpListener -import androidx.navigation.ui.NavigationUI.setupWithNavController -import info.cemu.cemu.R -import info.cemu.cemu.databinding.ActivitySettingsBinding -import java.util.Objects +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.navigation +import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute +import info.cemu.cemu.guicore.ActivityContent +import info.cemu.cemu.settings.audio.AudioSettingsScreen +import info.cemu.cemu.settings.gamespath.GamePathsScreen +import info.cemu.cemu.settings.general.GeneralSettingsScreen +import info.cemu.cemu.settings.graphics.GraphicsSettingsScreen +import info.cemu.cemu.settings.input.ControllerInputSettingsScreen +import info.cemu.cemu.settings.input.InputSettingsScreen +import info.cemu.cemu.settings.input.InputSettingsScreenActions +import info.cemu.cemu.settings.inputoverlay.InputOverlaySettingsScreen +import info.cemu.cemu.settings.overlay.OverlaySettingsScreen +import kotlinx.serialization.Serializable -class SettingsActivity : AppCompatActivity() { +class SettingsActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val binding = ActivitySettingsBinding.inflate( - layoutInflater - ) - setContentView(binding.root) - setSupportActionBar(binding.toolbarSettings) - val navHostFragment = - supportFragmentManager.findFragmentById(R.id.nav_host_fragment_content_settings) as NavHostFragment? - val navController = navHostFragment!!.navController - val appBarConfiguration: AppBarConfiguration = Builder() - .setFallbackOnNavigateUpListener(OnNavigateUpListener { this.onSupportNavigateUp() }) - .build() - setupWithNavController(binding.toolbarSettings, navController, appBarConfiguration) + setContent { + ActivityContent { + SettingsNav(parentNavBack = ::onNavigateUp) + } + } + } +} + +sealed class SettingsRoutes { + @Serializable + object GeneralSettingsRoute + + @Serializable + object GeneralSettingsScreenRoute + + @Serializable + object InputSettingsRoute + + @Serializable + object SettingsHomeScreenRoute + + @Serializable + object AudioSettingsScreenRoute + + @Serializable + object GraphicsSettingsScreenRoute + + @Serializable + object GamePathsScreenRoute + + @Serializable + object OverlaySettingsScreenRoute + + @Serializable + object InputSettingsScreenRoute + + @Serializable + data class ControllerInputSettingsScreenRoute(val index: Int) + + @Serializable + object InputOverlaySettingsScreenRoute +} + + +@Composable +fun SettingsNav( + parentNavBack: () -> Unit, +) { + val navController = rememberNavController() + + fun navigateBack() { + if (!navController.popBackStack()) { + parentNavBack() + } + } + + NavHost( + navController = navController, + startDestination = SettingsRoutes.SettingsHomeScreenRoute + ) { + composable { + SettingsHomeScreen( + navigateBack = ::navigateBack, + actions = SettingsHomeScreenActions( + goToGeneralSettings = { navController.navigate(SettingsRoutes.GeneralSettingsRoute) }, + goToInputSettings = { navController.navigate(SettingsRoutes.InputSettingsRoute) }, + goToGraphicsSettings = { navController.navigate(SettingsRoutes.GraphicsSettingsScreenRoute) }, + goToAudioSettings = { navController.navigate(SettingsRoutes.AudioSettingsScreenRoute) }, + goToOverlaySettings = { navController.navigate(SettingsRoutes.OverlaySettingsScreenRoute) }, + ) + ) + } + composable { + AudioSettingsScreen( + navigateBack = ::navigateBack, + ) + } + composable { + GraphicsSettingsScreen( + navigateBack = ::navigateBack, + ) + } + composable { + OverlaySettingsScreen( + navigateBack = ::navigateBack, + ) + } + navigation(startDestination = SettingsRoutes.InputSettingsScreenRoute) { + composable { navBackStackEntry -> + val controllerIndex = + navBackStackEntry.toRoute().index + ControllerInputSettingsScreen( + navigateBack = ::navigateBack, + controllerIndex = controllerIndex, + ) + } + composable { + InputOverlaySettingsScreen( + navigateBack = ::navigateBack + ) + } + composable { + InputSettingsScreen( + navigateBack = ::navigateBack, + actions = InputSettingsScreenActions( + goToInputOverlaySettings = { + navController.navigate(SettingsRoutes.InputOverlaySettingsScreenRoute) + }, + goToControllerSettings = { controllerIndex -> + navController.navigate( + SettingsRoutes.ControllerInputSettingsScreenRoute( + controllerIndex + ) + ) + }, + ) + ) + } + } + navigation(startDestination = SettingsRoutes.GeneralSettingsScreenRoute) { + composable { + GeneralSettingsScreen( + navigateBack = ::navigateBack, + goToGamePathsSettings = { navController.navigate(SettingsRoutes.GamePathsScreenRoute) } + ) + } + composable { + GamePathsScreen( + navigateBack = ::navigateBack, + ) + } + } } } \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsFragment.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsFragment.kt deleted file mode 100644 index 09960f0d..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsFragment.kt +++ /dev/null @@ -1,72 +0,0 @@ -package info.cemu.cemu.settings - -import android.content.ActivityNotFoundException -import android.content.Intent -import android.os.Bundle -import android.provider.DocumentsContract -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.NavHostFragment -import info.cemu.cemu.R -import info.cemu.cemu.databinding.LayoutGenericRecyclerViewBinding -import info.cemu.cemu.features.DocumentsProvider -import info.cemu.cemu.guibasecomponents.GenericRecyclerViewAdapter -import info.cemu.cemu.guibasecomponents.SimpleButtonRecyclerViewItem - -class SettingsFragment : Fragment() { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val binding = LayoutGenericRecyclerViewBinding.inflate(inflater, container, false) - val genericRecyclerViewAdapter = GenericRecyclerViewAdapter() - - genericRecyclerViewAdapter.addRecyclerViewItem(SimpleButtonRecyclerViewItem( - getString(R.string.open_cemu_folder) - ) { this.openCemuFolder() }) - val navController = NavHostFragment.findNavController(this) - genericRecyclerViewAdapter.addRecyclerViewItem(SimpleButtonRecyclerViewItem( - getString(R.string.general_settings) - ) { navController.navigate(R.id.action_settingsFragment_to_generalSettingsFragment) }) - genericRecyclerViewAdapter.addRecyclerViewItem(SimpleButtonRecyclerViewItem( - getString(R.string.input_settings) - ) { navController.navigate(R.id.action_settingsFragment_to_inputSettingsFragment) }) - genericRecyclerViewAdapter.addRecyclerViewItem(SimpleButtonRecyclerViewItem( - getString(R.string.graphics_settings) - ) { navController.navigate(R.id.action_settingsFragment_to_graphicSettingsFragment) }) - genericRecyclerViewAdapter.addRecyclerViewItem(SimpleButtonRecyclerViewItem( - getString(R.string.audio_settings) - ) { navController.navigate(R.id.action_settingsFragment_to_audioSettingsFragment) }) - genericRecyclerViewAdapter.addRecyclerViewItem(SimpleButtonRecyclerViewItem( - getString(R.string.graphic_packs) - ) { navController.navigate(R.id.action_settingsFragment_to_graphicPacksRootFragment) }) - genericRecyclerViewAdapter.addRecyclerViewItem(SimpleButtonRecyclerViewItem( - getString(R.string.overlay) - ) { navController.navigate(R.id.action_settingsFragment_to_overlaySettingsFragment) }) - binding.recyclerView.adapter = genericRecyclerViewAdapter - - return binding.root - } - - private fun openCemuFolder() { - try { - val intent = Intent(Intent.ACTION_VIEW) - intent.addCategory(Intent.CATEGORY_DEFAULT) - intent.setData( - DocumentsContract.buildRootUri( - DocumentsProvider.AUTHORITY, - DocumentsProvider.ROOT_ID - ) - ) - intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - requireContext().startActivity(intent) - } catch (activityNotFoundException: ActivityNotFoundException) { - Toast.makeText(requireContext(), R.string.failed_to_open_cemu_folder, Toast.LENGTH_LONG) - .show() - } - } -} \ No newline at end of file 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 new file mode 100644 index 00000000..3d629521 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsHomeScreen.kt @@ -0,0 +1,46 @@ +package info.cemu.cemu.settings + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.dropUnlessResumed +import info.cemu.cemu.R +import info.cemu.cemu.guicore.Button +import info.cemu.cemu.guicore.ScreenContent + +data class SettingsHomeScreenActions( + val goToGeneralSettings: () -> Unit, + val goToInputSettings: () -> Unit, + val goToGraphicsSettings: () -> Unit, + val goToAudioSettings: () -> Unit, + val goToOverlaySettings: () -> Unit, +) + +@Composable +fun SettingsHomeScreen(navigateBack: () -> Unit, actions: SettingsHomeScreenActions) { + ScreenContent( + appBarText = stringResource(R.string.settings), + navigateBack = navigateBack, + ) { + Button( + label = stringResource(R.string.general_settings), + onClick = dropUnlessResumed(block = actions.goToGeneralSettings) + ) + Button( + label = stringResource(R.string.input_settings), + onClick = dropUnlessResumed(block = actions.goToInputSettings) + ) + Button( + label = stringResource(R.string.graphics_settings), + onClick = dropUnlessResumed(block = actions.goToGraphicsSettings) + ) + Button( + label = stringResource(R.string.audio_settings), + onClick = dropUnlessResumed(block = actions.goToAudioSettings) + ) + Button( + label = stringResource(R.string.overlay_settings), + onClick = dropUnlessResumed(block = actions.goToOverlaySettings) + ) + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/audio/AudioSettingsFragment.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/audio/AudioSettingsFragment.kt deleted file mode 100644 index 8d84da38..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/audio/AudioSettingsFragment.kt +++ /dev/null @@ -1,131 +0,0 @@ -package info.cemu.cemu.settings.audio - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import info.cemu.cemu.R -import info.cemu.cemu.databinding.LayoutGenericRecyclerViewBinding -import info.cemu.cemu.guibasecomponents.GenericRecyclerViewAdapter -import info.cemu.cemu.guibasecomponents.SingleSelectionRecyclerViewItem -import info.cemu.cemu.guibasecomponents.SliderRecyclerViewItem -import info.cemu.cemu.guibasecomponents.ToggleRecyclerViewItem -import info.cemu.cemu.nativeinterface.NativeSettings -import info.cemu.cemu.nativeinterface.NativeSettings.audioLatency -import info.cemu.cemu.nativeinterface.NativeSettings.getAudioDeviceChannels -import info.cemu.cemu.nativeinterface.NativeSettings.getAudioDeviceEnabled -import info.cemu.cemu.nativeinterface.NativeSettings.getAudioDeviceVolume -import info.cemu.cemu.nativeinterface.NativeSettings.setAudioDeviceChannels -import info.cemu.cemu.nativeinterface.NativeSettings.setAudioDeviceEnabled -import info.cemu.cemu.nativeinterface.NativeSettings.setAudioDeviceVolume -import java.util.List - -class AudioSettingsFragment : Fragment() { - private fun channelsToString(channels: Int): String { - val resourceNameId = when (channels) { - NativeSettings.AUDIO_CHANNELS_MONO -> R.string.mono - NativeSettings.AUDIO_CHANNELS_STEREO -> R.string.stereo - NativeSettings.AUDIO_CHANNELS_SURROUND -> R.string.surround - else -> throw IllegalArgumentException("Invalid channels type: $channels") - } - return getString(resourceNameId) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val binding = LayoutGenericRecyclerViewBinding.inflate(inflater, container, false) - - val genericRecyclerViewAdapter = GenericRecyclerViewAdapter() - - val latencySlider = SliderRecyclerViewItem(getString(R.string.audio_latency), - 0f, - (NativeSettings.AUDIO_BLOCK_COUNT - 1).toFloat(), - audioLatency.toFloat(), - SliderRecyclerViewItem.OnChangeListener { value: Float -> audioLatency = value.toInt() } - ) { value: Float -> (value * 12).toInt().toString() + "ms" } - genericRecyclerViewAdapter.addRecyclerViewItem(latencySlider) - - val tvDeviceToggle = ToggleRecyclerViewItem(getString(R.string.tv), - getString(R.string.tv_audio_description), getAudioDeviceEnabled(true), - ToggleRecyclerViewItem.OnCheckedChangeListener { checked: Boolean -> - setAudioDeviceEnabled( - checked, - true - ) - }) - genericRecyclerViewAdapter.addRecyclerViewItem(tvDeviceToggle) - - val tvChannelsModeSelection = - SingleSelectionRecyclerViewItem(getString(R.string.tv_channels), - getAudioDeviceChannels(true), - List.of( - NativeSettings.AUDIO_CHANNELS_MONO, - NativeSettings.AUDIO_CHANNELS_STEREO, - NativeSettings.AUDIO_CHANNELS_SURROUND - ), - { channels: Int -> this.channelsToString(channels) }, - { channels: Int? -> - setAudioDeviceChannels( - channels!!, true - ) - }) - genericRecyclerViewAdapter.addRecyclerViewItem(tvChannelsModeSelection) - - val tvVolumeSlider = SliderRecyclerViewItem(getString(R.string.tv_volume), - NativeSettings.AUDIO_MIN_VOLUME.toFloat(), - NativeSettings.AUDIO_MAX_VOLUME.toFloat(), - getAudioDeviceVolume(true).toFloat(), - SliderRecyclerViewItem.OnChangeListener { value: Float -> - setAudioDeviceVolume( - value.toInt(), - true - ) - } - ) { value: Float -> value.toInt().toString() + "%" } - genericRecyclerViewAdapter.addRecyclerViewItem(tvVolumeSlider) - - val padDeviceToggle = ToggleRecyclerViewItem(getString(R.string.gamepad), - getString(R.string.gamepad_audio_description), getAudioDeviceEnabled(false), - ToggleRecyclerViewItem.OnCheckedChangeListener { checked: Boolean -> - setAudioDeviceEnabled( - checked, - false - ) - }) - genericRecyclerViewAdapter.addRecyclerViewItem(padDeviceToggle) - - val gamepadChannelsModeSelection = - SingleSelectionRecyclerViewItem(getString(R.string.gamepad_channels), - getAudioDeviceChannels(false), - List.of(NativeSettings.AUDIO_CHANNELS_STEREO), - { channels: Int -> this.channelsToString(channels) }, - { channels: Int? -> - setAudioDeviceChannels( - channels!!, false - ) - } - ) - genericRecyclerViewAdapter.addRecyclerViewItem(gamepadChannelsModeSelection) - - val padVolumeSlider = SliderRecyclerViewItem(getString(R.string.pad_volume), - NativeSettings.AUDIO_MIN_VOLUME.toFloat(), - NativeSettings.AUDIO_MAX_VOLUME.toFloat(), - getAudioDeviceVolume(false).toFloat(), - SliderRecyclerViewItem.OnChangeListener { value: Float -> - setAudioDeviceVolume( - value.toInt(), - false - ) - } - ) { value: Float -> value.toInt().toString() + "%" } - genericRecyclerViewAdapter.addRecyclerViewItem(padVolumeSlider) - - binding.recyclerView.adapter = genericRecyclerViewAdapter - - return binding.root - } -} \ No newline at end of file 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 new file mode 100644 index 00000000..ffb2a96d --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/audio/AudoSettingsScreen.kt @@ -0,0 +1,79 @@ +package info.cemu.cemu.settings.audio + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import info.cemu.cemu.R +import info.cemu.cemu.guicore.ScreenContent +import info.cemu.cemu.guicore.SingleSelection +import info.cemu.cemu.guicore.Slider +import info.cemu.cemu.guicore.Toggle +import info.cemu.cemu.guicore.enumtostringmapper.native.channelsToStringId +import info.cemu.cemu.nativeinterface.NativeSettings + +private const val AUDIO_LATENCY_STEPS = 22 + +@Composable +fun AudioSettingsScreen(navigateBack: () -> Unit) { + ScreenContent( + appBarText = stringResource(R.string.audio_settings), + navigateBack = navigateBack, + ) { + Slider( + label = stringResource(R.string.audio_latency), + initialValue = NativeSettings::getAudioLatency, + valueFrom = 0, + steps = AUDIO_LATENCY_STEPS, + valueTo = NativeSettings.AUDIO_LATENCY_MS_MAX, + onValueChange = NativeSettings::setAudioLatency, + labelFormatter = { "${it}ms" } + ) + Toggle( + label = stringResource(R.string.tv_audio), + description = stringResource(R.string.tv_audio_description), + initialCheckedState = { NativeSettings.getAudioDeviceEnabled(true) }, + onCheckedChanged = { NativeSettings.setAudioDeviceEnabled(it, true) } + ) + SingleSelection( + label = stringResource(R.string.tv_channels), + initialChoice = { NativeSettings.getAudioDeviceChannels(true) }, + onChoiceChanged = { NativeSettings.setAudioDeviceChannels(it, true) }, + choiceToString = { stringResource(channelsToStringId(it)) }, + choices = listOf( + NativeSettings.AUDIO_CHANNELS_MONO, + NativeSettings.AUDIO_CHANNELS_STEREO, + NativeSettings.AUDIO_CHANNELS_SURROUND, + ), + ) + Slider( + label = stringResource(R.string.tv_volume), + initialValue = { NativeSettings.getAudioDeviceVolume(true) }, + valueFrom = NativeSettings.AUDIO_MIN_VOLUME, + valueTo = NativeSettings.AUDIO_MAX_VOLUME, + onValueChange = { NativeSettings.setAudioDeviceVolume(it, true) }, + labelFormatter = { "$it%" } + ) + Toggle( + label = stringResource(R.string.gamepad_audio), + description = stringResource(R.string.gamepad_audio_description), + initialCheckedState = { NativeSettings.getAudioDeviceEnabled(false) }, + onCheckedChanged = { NativeSettings.setAudioDeviceEnabled(false, it) } + ) + SingleSelection( + label = stringResource(R.string.gamepad_channels), + initialChoice = { NativeSettings.getAudioDeviceChannels(false) }, + onChoiceChanged = { NativeSettings.setAudioDeviceChannels(it, false) }, + choiceToString = { stringResource(channelsToStringId(it)) }, + choices = listOf( + NativeSettings.AUDIO_CHANNELS_STEREO, + ), + ) + Slider( + label = stringResource(R.string.gamepad_volume), + initialValue = { NativeSettings.getAudioDeviceVolume(false) }, + valueFrom = NativeSettings.AUDIO_MIN_VOLUME, + valueTo = NativeSettings.AUDIO_MAX_VOLUME, + onValueChange = { NativeSettings.setAudioDeviceVolume(it, false) }, + labelFormatter = { "$it%" } + ) + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamePathAdapter.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamePathAdapter.kt deleted file mode 100644 index e033181a..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamePathAdapter.kt +++ /dev/null @@ -1,55 +0,0 @@ -package info.cemu.cemu.settings.gamespath - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.TextView -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import info.cemu.cemu.R - -class GamePathAdapter(private val onRemoveGamePath: OnRemoveGamePath) : - ListAdapter(DIFF_CALLBACK) { - fun interface OnRemoveGamePath { - fun onRemoveGamePath(path: String) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = - LayoutInflater.from(parent.context).inflate(R.layout.layout_game_path, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val gamePath = getItem(position) - holder.gamePath.text = gamePath - holder.gamePath.isSelected = true - holder.deleteButton.setOnClickListener { v: View? -> - onRemoveGamePath.onRemoveGamePath( - gamePath - ) - } - } - - class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var gamePath: TextView = - itemView.findViewById(R.id.game_path_text) - var deleteButton: Button = - itemView.findViewById(R.id.remove_game_path_button) - } - - companion object { - private val DIFF_CALLBACK: DiffUtil.ItemCallback = - object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: String, newItem: String): Boolean { - return oldItem == newItem - } - - override fun areContentsTheSame(oldItem: String, newItem: String): Boolean { - return oldItem == newItem - } - } - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamePathsFragment.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamePathsFragment.kt deleted file mode 100644 index 3403079a..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamePathsFragment.kt +++ /dev/null @@ -1,99 +0,0 @@ -package info.cemu.cemu.settings.gamespath - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.activity.result.ActivityResult -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.view.MenuProvider -import androidx.documentfile.provider.DocumentFile -import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle -import info.cemu.cemu.R -import info.cemu.cemu.databinding.LayoutGenericRecyclerViewBinding -import info.cemu.cemu.nativeinterface.NativeSettings -import info.cemu.cemu.nativeinterface.NativeSettings.addGamesPath -import info.cemu.cemu.nativeinterface.NativeSettings.removeGamesPath -import info.cemu.cemu.settings.gamespath.GamePathAdapter.OnRemoveGamePath -import java.util.Objects -import java.util.stream.Collectors -import java.util.stream.Stream - -class GamePathsFragment : Fragment() { - private var folderSelectionLauncher: ActivityResultLauncher? = null - private var gamePathAdapter: GamePathAdapter? = null - private var gamesPaths: List? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - folderSelectionLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result: ActivityResult -> - if (result.resultCode != Activity.RESULT_OK) { - return@registerForActivityResult - } - val data = result.data ?: return@registerForActivityResult - val uri = data.data!! - requireActivity().contentResolver.takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - val documentFile = DocumentFile.fromTreeUri(requireContext(), uri) - ?: return@registerForActivityResult - val gamesPath = documentFile.uri.toString() - if (gamesPaths!!.stream().anyMatch { p: String -> p == gamesPath }) { - Toast.makeText( - requireContext(), - R.string.game_path_already_added, - Toast.LENGTH_LONG - ).show() - return@registerForActivityResult - } - addGamesPath(gamesPath) - gamesPaths = Stream.concat( - Stream.of(gamesPath), gamesPaths!!.stream() - ) - .collect(Collectors.toList()) - gamePathAdapter!!.submitList(gamesPaths) - } - gamePathAdapter = GamePathAdapter { path: String -> - removeGamesPath(path) - gamesPaths = gamesPaths!!.filter { p: String -> p != path } - gamePathAdapter!!.submitList(gamesPaths) - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val binding = LayoutGenericRecyclerViewBinding.inflate(inflater, container, false) - binding.recyclerView.adapter = gamePathAdapter - gamesPaths = NativeSettings.gamesPaths - gamePathAdapter!!.submitList(gamesPaths) - requireActivity().addMenuProvider(object : MenuProvider { - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menuInflater.inflate(R.menu.game_paths, menu) - } - - override fun onMenuItemSelected(menuItem: MenuItem): Boolean { - if (menuItem.itemId == R.id.action_add_game_path) { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - folderSelectionLauncher!!.launch(intent) - return true - } - return false - } - }, viewLifecycleOwner, Lifecycle.State.RESUMED) - return binding.root - } -} \ No newline at end of file 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 new file mode 100644 index 00000000..e5706ba3 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamePathsScreen.kt @@ -0,0 +1,110 @@ +package info.cemu.cemu.settings.gamespath + +import android.content.Intent +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.viewmodel.compose.viewModel +import info.cemu.cemu.R +import info.cemu.cemu.guicore.ScreenContentLazy +import kotlinx.serialization.Serializable + +@Composable +fun GamePathsScreen( + navigateBack: () -> Unit, + gamesPathsViewModel: GamesPathsViewModel = viewModel() +) { + val context = LocalContext.current + val launcher = + rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> + if (uri == null) return@rememberLauncherForActivityResult + context.contentResolver.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + val documentFile = + DocumentFile.fromTreeUri(context, uri) ?: return@rememberLauncherForActivityResult + val gamesPath = documentFile.uri.toString() + if (gamesPathsViewModel.gamesPaths.contains(gamesPath)) { + Toast.makeText( + context, + R.string.games_path_already_added, + Toast.LENGTH_LONG + ).show() + return@rememberLauncherForActivityResult + } + gamesPathsViewModel.addGamesPath(gamesPath) + } + ScreenContentLazy( + appBarText = stringResource(R.string.game_paths_settings), + navigateBack = navigateBack, + actions = { + IconButton(onClick = { launcher.launch(null) }) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = stringResource(R.string.add_game_path), + ) + } + }, + ) { + items(gamesPathsViewModel.gamesPaths) { + GamePathsListItem( + modifier = Modifier.animateItem(), + gamesPath = it, + onDelete = { gamesPathsViewModel.removeGamesPath(it) } + ) + } + } +} + +@Composable +fun GamePathsListItem( + modifier: Modifier, + gamesPath: String, + onDelete: () -> Unit +) { + Card(modifier = modifier.padding(8.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth(), + ) { + Text( + modifier = Modifier + .weight(1.0f) + .padding(horizontal = 12.dp, vertical = 8.dp) + .basicMarquee(), + text = gamesPath, + maxLines = 1, + ) + IconButton( + modifier = Modifier.padding(top = 8.dp, bottom = 8.dp, end = 8.dp), + onClick = onDelete + ) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = stringResource(R.string.remove_game_path), + ) + } + } + } +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamesPathsViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamesPathsViewModel.kt new file mode 100644 index 00000000..a48369f0 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamesPathsViewModel.kt @@ -0,0 +1,28 @@ +package info.cemu.cemu.settings.gamespath + +import androidx.compose.runtime.mutableStateListOf +import androidx.lifecycle.ViewModel +import info.cemu.cemu.nativeinterface.NativeSettings + + + +class GamesPathsViewModel : ViewModel() { + private val _gamesPaths = mutableStateListOf().apply { + addAll(NativeSettings.getGamesPaths()) + } + val gamesPaths: List + get() = _gamesPaths + + fun addGamesPath(gamesPath: String) { + if (!_gamesPaths.contains(gamesPath)) { + _gamesPaths.add(gamesPath) + NativeSettings.addGamesPath(gamesPath) + } + } + + fun removeGamesPath(gamesPath: String) { + if (_gamesPaths.remove(gamesPath)) { + NativeSettings.removeGamesPath(gamesPath) + } + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsFragment.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsFragment.kt deleted file mode 100644 index 06ffe9c6..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsFragment.kt +++ /dev/null @@ -1,84 +0,0 @@ -package info.cemu.cemu.settings.general - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.NavHostFragment -import info.cemu.cemu.R -import info.cemu.cemu.databinding.LayoutGenericRecyclerViewBinding -import info.cemu.cemu.guibasecomponents.ButtonRecyclerViewItem -import info.cemu.cemu.guibasecomponents.GenericRecyclerViewAdapter -import info.cemu.cemu.guibasecomponents.SingleSelectionRecyclerViewItem -import info.cemu.cemu.nativeinterface.NativeSettings -import info.cemu.cemu.nativeinterface.NativeSettings.consoleLanguage -import java.util.List - -class GeneralSettingsFragment : Fragment() { - private fun consoleLanguageToString(channels: Int): String { - val resourceId = when (channels) { - NativeSettings.CONSOLE_LANGUAGE_JAPANESE -> R.string.console_language_japanese - NativeSettings.CONSOLE_LANGUAGE_ENGLISH -> R.string.console_language_english - NativeSettings.CONSOLE_LANGUAGE_FRENCH -> R.string.console_language_french - NativeSettings.CONSOLE_LANGUAGE_GERMAN -> R.string.console_language_german - NativeSettings.CONSOLE_LANGUAGE_ITALIAN -> R.string.console_language_italian - NativeSettings.CONSOLE_LANGUAGE_SPANISH -> R.string.console_language_spanish - NativeSettings.CONSOLE_LANGUAGE_CHINESE -> R.string.console_language_chinese - NativeSettings.CONSOLE_LANGUAGE_KOREAN -> R.string.console_language_korean - NativeSettings.CONSOLE_LANGUAGE_DUTCH -> R.string.console_language_dutch - NativeSettings.CONSOLE_LANGUAGE_PORTUGUESE -> R.string.console_language_portuguese - NativeSettings.CONSOLE_LANGUAGE_RUSSIAN -> R.string.console_language_russian - NativeSettings.CONSOLE_LANGUAGE_TAIWANESE -> R.string.console_language_taiwanese - else -> throw IllegalArgumentException("Invalid console language: $channels") - } - return getString(resourceId) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val binding = LayoutGenericRecyclerViewBinding.inflate(inflater, container, false) - - val genericRecyclerViewAdapter = GenericRecyclerViewAdapter() - - genericRecyclerViewAdapter.addRecyclerViewItem( - ButtonRecyclerViewItem( - getString(R.string.add_game_path), - getString(R.string.games_folder_description) - ) { - NavHostFragment.findNavController( - this@GeneralSettingsFragment - ).navigate(R.id.action_generalSettingsFragment_to_gamePathsFragment) - } - ) - - val consoleLanguageSelection = SingleSelectionRecyclerViewItem( - getString(R.string.console_language), - consoleLanguage, - listOf( - NativeSettings.CONSOLE_LANGUAGE_JAPANESE, - NativeSettings.CONSOLE_LANGUAGE_ENGLISH, - NativeSettings.CONSOLE_LANGUAGE_FRENCH, - NativeSettings.CONSOLE_LANGUAGE_GERMAN, - NativeSettings.CONSOLE_LANGUAGE_ITALIAN, - NativeSettings.CONSOLE_LANGUAGE_SPANISH, - NativeSettings.CONSOLE_LANGUAGE_CHINESE, - NativeSettings.CONSOLE_LANGUAGE_KOREAN, - NativeSettings.CONSOLE_LANGUAGE_DUTCH, - NativeSettings.CONSOLE_LANGUAGE_PORTUGUESE, - NativeSettings.CONSOLE_LANGUAGE_RUSSIAN, - NativeSettings.CONSOLE_LANGUAGE_TAIWANESE - ), - { channels: Int -> this.consoleLanguageToString(channels) }, - { newConsoleLanguage -> NativeSettings::consoleLanguage.set(newConsoleLanguage) } - ) - genericRecyclerViewAdapter.addRecyclerViewItem(consoleLanguageSelection) - - binding.recyclerView.adapter = genericRecyclerViewAdapter - - return binding.root - } -} \ No newline at end of file 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 new file mode 100644 index 00000000..3f8ff9b2 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsScreen.kt @@ -0,0 +1,45 @@ +package info.cemu.cemu.settings.general + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.dropUnlessResumed +import info.cemu.cemu.R +import info.cemu.cemu.guicore.Button +import info.cemu.cemu.guicore.ScreenContent +import info.cemu.cemu.guicore.SingleSelection +import info.cemu.cemu.guicore.enumtostringmapper.native.consoleLanguageToStringId +import info.cemu.cemu.nativeinterface.NativeSettings + +@Composable +fun GeneralSettingsScreen(navigateBack: () -> Unit, goToGamePathsSettings: () -> Unit) { + ScreenContent( + appBarText = stringResource(R.string.general_settings), + navigateBack = navigateBack, + ) { + Button( + label = stringResource(R.string.add_game_path), + description = stringResource(R.string.games_folder_description), + onClick = dropUnlessResumed { goToGamePathsSettings() }, + ) + SingleSelection( + label = stringResource(R.string.console_language), + initialChoice = NativeSettings::getConsoleLanguage, + onChoiceChanged = NativeSettings::setConsoleLanguage, + choiceToString = { stringResource(consoleLanguageToStringId(it)) }, + choices = listOf( + NativeSettings.CONSOLE_LANGUAGE_JAPANESE, + NativeSettings.CONSOLE_LANGUAGE_ENGLISH, + NativeSettings.CONSOLE_LANGUAGE_FRENCH, + NativeSettings.CONSOLE_LANGUAGE_GERMAN, + NativeSettings.CONSOLE_LANGUAGE_ITALIAN, + NativeSettings.CONSOLE_LANGUAGE_SPANISH, + NativeSettings.CONSOLE_LANGUAGE_CHINESE, + NativeSettings.CONSOLE_LANGUAGE_KOREAN, + NativeSettings.CONSOLE_LANGUAGE_DUTCH, + NativeSettings.CONSOLE_LANGUAGE_PORTUGUESE, + NativeSettings.CONSOLE_LANGUAGE_RUSSIAN, + NativeSettings.CONSOLE_LANGUAGE_TAIWANESE, + ), + ) + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackListItemRecyclerViewItem.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackListItemRecyclerViewItem.kt deleted file mode 100644 index feaaeb77..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackListItemRecyclerViewItem.kt +++ /dev/null @@ -1,90 +0,0 @@ -package info.cemu.cemu.settings.graphicpacks - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.card.MaterialCardView -import com.google.android.material.textview.MaterialTextView -import info.cemu.cemu.R -import info.cemu.cemu.guibasecomponents.RecyclerViewItem - -class GraphicPackListItemRecyclerViewItem( - node: GraphicPackNode, - private val onClickCallback: OnClickCallback -) : - RecyclerViewItem { - private class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var title: MaterialTextView = - itemView.findViewById(R.id.graphic_pack_text) - var icon: ImageView = - itemView.findViewById(R.id.graphic_pack_icon) - var enabledGraphicPacksCount: MaterialTextView = - itemView.findViewById(R.id.graphic_pack_enabled_count) - var enabledIcon: ImageView = - itemView.findViewById(R.id.graphic_pack_enabled_icon) - var graphicPackExtraInfo: MaterialCardView = - itemView.findViewById(R.id.graphic_pack_extra_info) - } - - fun interface OnClickCallback { - fun onClick() - } - - - private val text: String? - - @JvmField - val titleIdInstalled: Boolean - private val graphicPackNode: GraphicPackNode - - init { - this.text = node.name - this.titleIdInstalled = node.titleIdInstalled - graphicPackNode = node - } - - override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { - return ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.layout_graphic_pack_item, parent, false) - ) - } - - override fun onBindViewHolder( - viewHolder: RecyclerView.ViewHolder, - adapter: RecyclerView.Adapter - ) { - val graphicPackViewHolder = viewHolder as ViewHolder - graphicPackViewHolder.itemView.setOnClickListener { v: View? -> onClickCallback.onClick() } - graphicPackViewHolder.title.text = text - if (graphicPackNode is GraphicPackSectionNode) { - configureGraphicPackSection(graphicPackViewHolder, graphicPackNode) - } else if (graphicPackNode is GraphicPackDataNode) { - configureGraphicPack(graphicPackViewHolder, graphicPackNode) - } - } - - private fun configureGraphicPackSection( - viewHolder: ViewHolder, - sectionNode: GraphicPackSectionNode - ) { - viewHolder.icon.setImageResource(R.drawable.ic_lists) - val enabledGraphicPacksCount = sectionNode.enabledGraphicPacksCount - if (enabledGraphicPacksCount == 0) { - return - } - val graphicPacksCountText = - if (enabledGraphicPacksCount >= 100) "99+" else enabledGraphicPacksCount.toString() - viewHolder.enabledGraphicPacksCount.text = graphicPacksCountText - viewHolder.enabledGraphicPacksCount.visibility = View.VISIBLE - } - - private fun configureGraphicPack(viewHolder: ViewHolder, dataNode: GraphicPackDataNode) { - viewHolder.icon.setImageResource(R.drawable.ic_package_2) - if (dataNode.enabled) { - viewHolder.enabledIcon.visibility = View.VISIBLE - } - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackRecyclerViewItem.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackRecyclerViewItem.kt deleted file mode 100644 index cdbf9490..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPackRecyclerViewItem.kt +++ /dev/null @@ -1,55 +0,0 @@ -package info.cemu.cemu.settings.graphicpacks - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.CompoundButton -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.materialswitch.MaterialSwitch -import info.cemu.cemu.R -import info.cemu.cemu.guibasecomponents.RecyclerViewItem -import info.cemu.cemu.nativeinterface.NativeGraphicPacks.GraphicPack - -class GraphicPackRecyclerViewItem( - private val graphicPack: GraphicPack, - private val onCheckedChangeListener: OnCheckedChangeListener? -) : - RecyclerViewItem { - private class GraphicPackViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var name: TextView = - itemView.findViewById(R.id.graphic_pack_name) - var enableToggle: MaterialSwitch = - itemView.findViewById(R.id.graphic_pack_enable_toggle) - var description: TextView = - itemView.findViewById(R.id.graphic_pack_description) - } - - fun interface OnCheckedChangeListener { - fun onCheckedChange(checked: Boolean) - } - - override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { - return GraphicPackViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.layout_graphic_pack, parent, false) - ) - } - - override fun onBindViewHolder( - viewHolder: RecyclerView.ViewHolder, - recyclerViewAdapter: RecyclerView.Adapter - ) { - val graphicPackViewHolder = viewHolder as GraphicPackViewHolder - graphicPackViewHolder.name.text = graphicPack.name - if (graphicPack.description != null) { - graphicPackViewHolder.description.text = graphicPack.description - } else { - graphicPackViewHolder.description.setText(R.string.graphic_pack_no_description) - } - graphicPackViewHolder.enableToggle.isChecked = graphicPack.isActive() - graphicPackViewHolder.enableToggle.setOnCheckedChangeListener { materialCheckBox: CompoundButton?, isChecked: Boolean -> - graphicPack.setActive(isChecked) - onCheckedChangeListener?.onCheckedChange(isChecked) - } - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPacksFragment.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPacksFragment.kt deleted file mode 100644 index 57b26079..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPacksFragment.kt +++ /dev/null @@ -1,118 +0,0 @@ -package info.cemu.cemu.settings.graphicpacks - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.fragment.NavHostFragment -import info.cemu.cemu.R -import info.cemu.cemu.databinding.LayoutGenericRecyclerViewBinding -import info.cemu.cemu.guibasecomponents.GenericRecyclerViewAdapter -import info.cemu.cemu.guibasecomponents.SingleSelectionRecyclerViewItem -import info.cemu.cemu.guibasecomponents.StringSelectionAdapter -import info.cemu.cemu.nativeinterface.NativeGraphicPacks.GraphicPack -import info.cemu.cemu.nativeinterface.NativeGraphicPacks.getGraphicPack -import java.util.Objects -import java.util.function.Consumer -import java.util.stream.IntStream - -class GraphicPacksFragment : Fragment() { - private val genericRecyclerViewAdapter = GenericRecyclerViewAdapter() - private var graphicPackPreviousViewModel: GraphicPackViewModel? = null - private var graphicPackCurrentViewModel: GraphicPackViewModel? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val navController = NavHostFragment.findNavController(this) - graphicPackPreviousViewModel = - ViewModelProvider(navController.previousBackStackEntry!!) - .get(GraphicPackViewModel::class.java) - graphicPackCurrentViewModel = - ViewModelProvider(navController.currentBackStackEntry!!) - .get(GraphicPackViewModel::class.java) - fillData() - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val binding = LayoutGenericRecyclerViewBinding.inflate(inflater, container, false) - binding.recyclerView.adapter = genericRecyclerViewAdapter - - return binding.root - } - - private fun fillData() { - val graphicNode = graphicPackPreviousViewModel!!.graphicPackNode - if (graphicNode is GraphicPackSectionNode) { - fillData(graphicNode) - } else if (graphicNode is GraphicPackDataNode) { - fillData(graphicNode) - } - } - - private fun fillData(graphicPackSectionNode: GraphicPackSectionNode) { - graphicPackSectionNode.children.forEach(Consumer { node: GraphicPackNode -> - val graphicPackItemRecyclerViewItem = GraphicPackListItemRecyclerViewItem(node, - GraphicPackListItemRecyclerViewItem.OnClickCallback { - graphicPackCurrentViewModel!!.graphicPackNode = node - val bundle = Bundle() - bundle.putString("title", node.name) - NavHostFragment.findNavController(this@GraphicPacksFragment) - .navigate(R.id.action_graphicPacksFragment_self, bundle) - }) - genericRecyclerViewAdapter.addRecyclerViewItem(graphicPackItemRecyclerViewItem) - }) - } - - private fun fillData(graphicPackDataNode: GraphicPackDataNode) { - genericRecyclerViewAdapter.clearRecyclerViewItems() - val graphicPack = getGraphicPack(graphicPackDataNode.id) - genericRecyclerViewAdapter.addRecyclerViewItem( - GraphicPackRecyclerViewItem( - graphicPack!! - ) { enabled: Boolean -> - graphicPackDataNode.enabled = enabled - } - ) - fillPresets(graphicPack) - } - - private fun fillPresets(graphicPack: GraphicPack) { - val recyclerViewItems = ArrayList>() - IntStream.range(0, graphicPack.getPresets().size).forEach { index: Int -> - val graphicPackPreset = graphicPack.getPresets()[index] - var category = graphicPackPreset.category - if (category == null) { - category = getString(R.string.active_preset_category) - } - val recyclerViewItem = SingleSelectionRecyclerViewItem( - category, - graphicPackPreset.getActivePreset(), - StringSelectionAdapter( - graphicPackPreset.presets, - graphicPackPreset.getActivePreset() - ) - ) { selectedValue: String?, selectionRecyclerViewItem: SingleSelectionRecyclerViewItem -> - val oldPresets = graphicPack.getPresets() - graphicPack.getPresets()[index].setActivePreset(selectedValue!!) - selectionRecyclerViewItem.setDescription(selectedValue) - graphicPack.reloadPresets() - if (oldPresets != graphicPack.getPresets()) { - recyclerViewItems.forEach(Consumer { recyclerViewItem: SingleSelectionRecyclerViewItem? -> - genericRecyclerViewAdapter.removeRecyclerViewItem( - recyclerViewItem!! - ) - }) - fillPresets(graphicPack) - } - } - recyclerViewItems.add(recyclerViewItem) - genericRecyclerViewAdapter.addRecyclerViewItem(recyclerViewItem) - } - } -} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPacksRecyclerViewAdapter.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPacksRecyclerViewAdapter.kt deleted file mode 100644 index d7ae9d44..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPacksRecyclerViewAdapter.kt +++ /dev/null @@ -1,168 +0,0 @@ -package info.cemu.cemu.settings.graphicpacks - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.card.MaterialCardView -import com.google.android.material.textview.MaterialTextView -import info.cemu.cemu.R -import info.cemu.cemu.guibasecomponents.RecyclerViewItem -import java.util.Arrays -import java.util.regex.Pattern -import java.util.stream.Collectors - -class GraphicPacksRecyclerViewAdapter(private val onItemClickCallback: OnItemClickCallback) : - RecyclerView.Adapter() { - private class GraphicPackSearchItemRecyclerViewItem( - val dataNode: GraphicPackDataNode, - private val onClickCallback: OnClickCallback - ) : - RecyclerViewItem { - private class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var name: MaterialTextView = - itemView.findViewById(R.id.graphic_pack_search_name) - var path: MaterialTextView = - itemView.findViewById(R.id.graphic_pack_search_path) - var enabledIcon: MaterialCardView = - itemView.findViewById(R.id.graphic_pack_enabled_icon) - } - - fun interface OnClickCallback { - fun onClick() - } - - val titleIdInstalled: Boolean = dataNode.titleIdInstalled - - override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { - return ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.layout_graphic_pack_search_item, parent, false) - ) - } - - override fun onBindViewHolder( - viewHolder: RecyclerView.ViewHolder, - adapter: RecyclerView.Adapter - ) { - val graphicPackViewHolder = viewHolder as ViewHolder - graphicPackViewHolder.itemView.setOnClickListener { v: View? -> onClickCallback.onClick() } - graphicPackViewHolder.name.text = dataNode.name - graphicPackViewHolder.path.text = dataNode.path - if (dataNode.enabled) { - graphicPackViewHolder.enabledIcon.visibility = View.VISIBLE - } - } - } - - fun interface OnItemClickCallback { - fun onClick(graphicPackDataNode: GraphicPackDataNode) - } - - private var showInstalledGamesOnly = false - private var filterText = "" - - private var recyclerViewItems: MutableList = ArrayList() - private var filteredViewItems: MutableList = ArrayList() - - fun setShowInstalledOnly(showInstalledGamesOnly: Boolean) { - this.showInstalledGamesOnly = showInstalledGamesOnly - filter() - } - - fun setItems(graphicPackDataNodes: List) { - clearItems() - recyclerViewItems = graphicPackDataNodes.stream() - .sorted(Comparator.comparing(GraphicPackDataNode::path)) - .map { graphicPackDataNode: GraphicPackDataNode -> - GraphicPackSearchItemRecyclerViewItem( - graphicPackDataNode, - GraphicPackSearchItemRecyclerViewItem.OnClickCallback { - onItemClickCallback.onClick( - graphicPackDataNode - ) - } - ) - }.collect(Collectors.toList()) - filteredViewItems = recyclerViewItems - if (showInstalledGamesOnly) { - filteredViewItems = filteredViewItems.stream() - .filter { g: GraphicPackSearchItemRecyclerViewItem -> g.titleIdInstalled } - .collect( - Collectors.toList() - ) - } - notifyItemRangeInserted(0, filteredViewItems.size) - } - - private fun resetFilteredItems() { - filteredViewItems = recyclerViewItems - if (showInstalledGamesOnly) { - filteredViewItems = filteredViewItems.stream() - .filter { g: GraphicPackSearchItemRecyclerViewItem -> g.titleIdInstalled } - .collect( - Collectors.toList() - ) - } - notifyDataSetChanged() - } - - fun setFilterText(filterText: String) { - this.filterText = filterText - filter() - } - - fun filter() { - if (filterText.isBlank()) { - resetFilteredItems() - return - } - val stringBuilder = StringBuilder() - val pattern = Arrays.stream(filterText.split(" ".toRegex()).dropLastWhile { it.isEmpty() } - .toTypedArray()) - .map { s: String? -> "(?=.*" + Pattern.quote(s) + ")" } - .collect( - { stringBuilder }, - { obj: StringBuilder, str: String? -> obj.append(str) }, - { obj: StringBuilder, s: StringBuilder? -> obj.append(s) }) - .append(".*") - .toString() - val regex = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE) - var filteredData = recyclerViewItems.stream() - if (showInstalledGamesOnly) { - filteredData = - filteredData.filter { g: GraphicPackSearchItemRecyclerViewItem -> g.titleIdInstalled } - } - filteredData = filteredData.filter { g: GraphicPackSearchItemRecyclerViewItem -> - regex.matcher(g.dataNode.path).matches() - } - filteredViewItems = filteredData.collect(Collectors.toList()) - notifyDataSetChanged() - } - - fun clearItems() { - if (filteredViewItems.isEmpty()) { - return - } - val itemsCount = filteredViewItems.size - filteredViewItems.clear() - recyclerViewItems.clear() - notifyItemRangeRemoved(0, itemsCount) - } - - override fun onCreateViewHolder(parent: ViewGroup, position: Int): RecyclerView.ViewHolder { - return filteredViewItems[position].onCreateViewHolder(parent) - } - - override fun getItemViewType(position: Int): Int { - return position - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - filteredViewItems[position].onBindViewHolder(holder, this) - } - - override fun getItemCount(): Int { - return filteredViewItems.size - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPacksRootFragment.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPacksRootFragment.kt deleted file mode 100644 index 2c470702..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/graphicpacks/GraphicPacksRootFragment.kt +++ /dev/null @@ -1,360 +0,0 @@ -package info.cemu.cemu.settings.graphicpacks - -import android.content.DialogInterface -import android.os.Bundle -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.widget.SearchView -import androidx.core.view.MenuProvider -import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.fragment.NavHostFragment -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar -import info.cemu.cemu.R -import info.cemu.cemu.databinding.LayoutGenericRecyclerViewBinding -import info.cemu.cemu.guibasecomponents.FilterableRecyclerViewAdapter -import info.cemu.cemu.io.Files.delete -import info.cemu.cemu.nativeinterface.NativeGameTitles.installedGamesTitleIds -import info.cemu.cemu.nativeinterface.NativeGraphicPacks.GraphicPackBasicInfo -import info.cemu.cemu.nativeinterface.NativeGraphicPacks.graphicPackBasicInfos -import info.cemu.cemu.nativeinterface.NativeGraphicPacks.refreshGraphicPacks -import info.cemu.cemu.zip.unzip -import okhttp3.Call -import okhttp3.Callback -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Request.Builder -import okhttp3.Response -import org.json.JSONException -import org.json.JSONObject -import java.io.IOException -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.nio.file.Paths -import java.util.Objects -import java.util.Optional -import java.util.function.Consumer -import kotlin.jvm.optionals.getOrNull - -class GraphicPacksRootFragment : Fragment(), MenuProvider { - private var installedOnly = true - private val installedTitleIds = installedGamesTitleIds - private val graphicPackSectionRootNode = GraphicPackSectionNode() - private val genericRecyclerViewAdapter = - FilterableRecyclerViewAdapter() - private var graphicPackViewModel: GraphicPackViewModel? = null - private var recyclerView: RecyclerView? = null - private val graphicPacksRecyclerViewAdapter = - GraphicPacksRecyclerViewAdapter { graphicPackDataNode: GraphicPackDataNode -> - graphicPackViewModel!!.graphicPackNode = graphicPackDataNode - val bundle = Bundle() - bundle.putString("title", graphicPackDataNode.name) - NavHostFragment.findNavController(this@GraphicPacksRootFragment) - .navigate(R.id.action_graphicPacksRootFragment_to_graphicPacksFragment, bundle) - } - private val client = OkHttpClient() - private var updateInProgress = false - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - genericRecyclerViewAdapter.setPredicate { g: GraphicPackListItemRecyclerViewItem -> g.titleIdInstalled } - graphicPacksRecyclerViewAdapter.setShowInstalledOnly(this.installedOnly) - fillGraphicPacks() - val navController = NavHostFragment.findNavController(this@GraphicPacksRootFragment) - graphicPackViewModel = - ViewModelProvider(navController.currentBackStackEntry!!) - .get(GraphicPackViewModel::class.java) - } - - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menuInflater.inflate(R.menu.graphic_packs, menu) - val searchMenuItem = menu.findItem(R.id.action_graphic_packs_search) - menu.findItem(R.id.show_installed_games_only).setChecked(installedOnly) - val searchView = Objects.requireNonNull(searchMenuItem.actionView) as SearchView - searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String): Boolean { - graphicPacksRecyclerViewAdapter.setFilterText(query) - return true - } - - override fun onQueryTextChange(newText: String): Boolean { - graphicPacksRecyclerViewAdapter.setFilterText(newText) - return true - } - }) - searchMenuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { - override fun onMenuItemActionExpand(item: MenuItem): Boolean { - if (recyclerView == null) { - return true - } - recyclerView!!.adapter = graphicPacksRecyclerViewAdapter - return true - } - - override fun onMenuItemActionCollapse(item: MenuItem): Boolean { - if (recyclerView == null) { - return true - } - recyclerView!!.adapter = genericRecyclerViewAdapter - return true - } - }) - } - - private fun showInstalledOnly(installedOnly: Boolean) { - this.installedOnly = installedOnly - if (installedOnly) { - genericRecyclerViewAdapter.setPredicate { g: GraphicPackListItemRecyclerViewItem -> g.titleIdInstalled } - return - } - genericRecyclerViewAdapter.clearPredicate() - } - - override fun onMenuItemSelected(menuItem: MenuItem): Boolean { - if (menuItem.itemId == R.id.action_graphic_packs_download && !updateInProgress) { - checkForUpdates() - return true - } - if (menuItem.itemId == R.id.action_graphic_packs_search) { - return true - } - if (menuItem.itemId == R.id.show_installed_games_only) { - val installedOnly = !menuItem.isChecked - graphicPacksRecyclerViewAdapter.setShowInstalledOnly(installedOnly) - menuItem.setChecked(installedOnly) - showInstalledOnly(installedOnly) - return true - } - return false - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - val binding = LayoutGenericRecyclerViewBinding.inflate(inflater, container, false) - recyclerView = binding.recyclerView - recyclerView!!.adapter = genericRecyclerViewAdapter - requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) - return binding.root - } - - private fun setDataNodes( - graphicPackDataNodes: MutableList, - graphicPackSectionNode: GraphicPackSectionNode - ) { - for (node in graphicPackSectionNode.children) { - if (node is GraphicPackSectionNode) { - setDataNodes(graphicPackDataNodes, node) - } else if (node is GraphicPackDataNode) { - graphicPackDataNodes.add(node) - } - } - } - - private fun fillGraphicPacks() { - val nativeGraphicPacks = graphicPackBasicInfos - graphicPackSectionRootNode.clear() - val graphicPackDataNodes: MutableList = ArrayList() - nativeGraphicPacks!!.forEach(Consumer { graphicPack: GraphicPackBasicInfo? -> - val hasTitleIdInstalled = graphicPack!!.titleIds.stream() - .anyMatch { o: Long? -> installedTitleIds!!.contains(o) } - graphicPackSectionRootNode.addGraphicPackDataByTokens(graphicPack, hasTitleIdInstalled) - }) - - setDataNodes(graphicPackDataNodes, graphicPackSectionRootNode) - graphicPacksRecyclerViewAdapter.setItems(graphicPackDataNodes) - - genericRecyclerViewAdapter.clearRecyclerViewItems() - graphicPackSectionRootNode.sort() - graphicPackSectionRootNode.children.forEach(Consumer { node: GraphicPackNode -> - val graphicPackItemRecyclerViewItem = GraphicPackListItemRecyclerViewItem( - node - ) { - graphicPackViewModel!!.graphicPackNode = node - val bundle = Bundle() - bundle.putString("title", node.name) - NavHostFragment.findNavController(this@GraphicPacksRootFragment) - .navigate(R.id.action_graphicPacksRootFragment_to_graphicPacksFragment, bundle) - } - genericRecyclerViewAdapter.addRecyclerViewItem(graphicPackItemRecyclerViewItem) - }) - } - - private fun checkForUpdates() { - val dialog = createGraphicPacksDownloadDialog() - requireActivity().runOnUiThread { - (dialog.findViewById(R.id.textViewGraphicPacksDownload) as TextView) - .setText(R.string.checking_version_download_text) - } - val request: Request = Builder() - .url(RELEASES_API_URL) - .build() - requireActivity().runOnUiThread { - (dialog.findViewById(R.id.textViewGraphicPacksDownload) as TextView) - .setText(R.string.graphic_packs_download_text) - } - client.newCall(request).enqueue(object : Callback { - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - try { - response.body?.use { responseBody -> - val json = JSONObject(Objects.requireNonNull(responseBody).string()) - val version = json.getString("name") - if (currentVersion.getOrNull() == version) { - onGraphicPacksLatestVersion(dialog) - return - } - val downloadUrl = json.getJSONArray("assets") - .getJSONObject(0) - .getString("browser_download_url") - downloadNewUpdate(downloadUrl, version, dialog) - } - } catch (e: JSONException) { - requireActivity().runOnUiThread { dialog.dismiss() } - } - } - - override fun onFailure(call: Call, e: IOException) { - if (!call.isCanceled()) { - onDownloadError(dialog) - } - } - }) - } - - private val currentVersion: Optional - get() { - val graphicPacksVersionFile = Paths.get( - requireActivity().getExternalFilesDir( - null - )!!.absolutePath, - "graphicPacks", - "downloadedGraphicPacks", - "version.txt" - ) - return try { - Optional.of( - String( - Files.readAllBytes( - graphicPacksVersionFile - ) - ) - ) - } catch (ignored: IOException) { - Optional.empty() - } - } - - private fun downloadNewUpdate( - graphicPacksZipDownloadUrl: String, - version: String, - dialog: AlertDialog - ) { - val request: Request = Builder() - .url(graphicPacksZipDownloadUrl) - .build() - - val graphicPacksDirPath = Paths.get( - requireActivity().getExternalFilesDir(null)!!.absolutePath, - "graphicPacks" - ) - client.newCall(request).enqueue(object : Callback { - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - response.body?.use { responseBody -> - val graphicPacksTempDirPath = - graphicPacksDirPath.resolve("downloadedGraphicPacksTemp") - delete(graphicPacksTempDirPath.toFile()) - unzip( - responseBody.byteStream(), - graphicPacksTempDirPath.toString() - ) - Files.write( - graphicPacksTempDirPath.resolve("version.txt"), version.toByteArray( - StandardCharsets.UTF_8 - ) - ) - val downloadedGraphicPacksDirPath = - graphicPacksDirPath.resolve("downloadedGraphicPacks") - delete(downloadedGraphicPacksDirPath.toFile()) - Files.move(graphicPacksTempDirPath, downloadedGraphicPacksDirPath) - refreshGraphicPacks() - requireActivity().runOnUiThread { fillGraphicPacks() } - onDownloadFinish(dialog) - } - } - - override fun onFailure(call: Call, e: IOException) { - if (!call.isCanceled()) { - onDownloadError(dialog) - } - } - }) - } - - private fun onDownloadError(dialog: AlertDialog) { - requireActivity().runOnUiThread { - dialog.dismiss() - Snackbar.make( - requireView(), - R.string.download_graphic_packs_error_text, - Snackbar.LENGTH_SHORT - ).show() - } - } - - private fun onDownloadFinish(dialog: AlertDialog) { - requireActivity().runOnUiThread { - dialog.dismiss() - Snackbar.make( - requireView(), - R.string.download_graphic_packs_finish_text, - Snackbar.LENGTH_SHORT - ).show() - } - } - - private fun onGraphicPacksLatestVersion(dialog: AlertDialog) { - requireActivity().runOnUiThread { - dialog.dismiss() - Snackbar.make( - requireView(), - R.string.graphic_packs_no_updates_text, - Snackbar.LENGTH_SHORT - ).show() - } - } - - private fun createGraphicPacksDownloadDialog(): AlertDialog { - updateInProgress = true - return MaterialAlertDialogBuilder(requireContext()).setView(R.layout.layout_graphic_packs_download) - .setTitle(R.string.graphic_packs_download_dialog_title) - .setCancelable(false) - .setOnDismissListener { d: DialogInterface? -> updateInProgress = false } - .setNegativeButton( - R.string.cancel - ) { dialogInterface: DialogInterface?, i: Int -> client.dispatcher.cancelAll() } - .show() - } - - override fun onDestroy() { - client.dispatcher.cancelAll() - super.onDestroy() - } - - companion object { - private const val RELEASES_API_URL = - "https://api.github.com/repos/cemu-project/cemu_graphic_packs/releases/latest" - } -} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/graphics/GraphicsSettingsFragment.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/graphics/GraphicsSettingsFragment.kt deleted file mode 100644 index c65bc561..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/graphics/GraphicsSettingsFragment.kt +++ /dev/null @@ -1,131 +0,0 @@ -package info.cemu.cemu.settings.graphics - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import info.cemu.cemu.R -import info.cemu.cemu.databinding.LayoutGenericRecyclerViewBinding -import info.cemu.cemu.guibasecomponents.GenericRecyclerViewAdapter -import info.cemu.cemu.guibasecomponents.SingleSelectionRecyclerViewItem -import info.cemu.cemu.guibasecomponents.ToggleRecyclerViewItem -import info.cemu.cemu.nativeinterface.NativeSettings -import info.cemu.cemu.nativeinterface.NativeSettings.accurateBarriers -import info.cemu.cemu.nativeinterface.NativeSettings.asyncShaderCompile -import info.cemu.cemu.nativeinterface.NativeSettings.downscalingFilter -import info.cemu.cemu.nativeinterface.NativeSettings.fullscreenScaling -import info.cemu.cemu.nativeinterface.NativeSettings.upscalingFilter -import info.cemu.cemu.nativeinterface.NativeSettings.vSyncMode -import java.util.List - -class GraphicsSettingsFragment : Fragment() { - private fun vsyncModeToString(vsyncMode: Int): String { - val resourceId = when (vsyncMode) { - NativeSettings.VSYNC_MODE_OFF -> R.string.off - NativeSettings.VSYNC_MODE_DOUBLE_BUFFERING -> R.string.double_buffering - NativeSettings.VSYNC_MODE_TRIPLE_BUFFERING -> R.string.triple_buffering - else -> throw IllegalArgumentException("Invalid vsync mode: $vsyncMode") - } - return getString(resourceId) - } - - private fun fullscreenScalingModeToString(fullscreenScaling: Int): String { - val resourceId = when (fullscreenScaling) { - NativeSettings.FULLSCREEN_SCALING_KEEP_ASPECT_RATIO -> R.string.keep_aspect_ratio - NativeSettings.FULLSCREEN_SCALING_STRETCH -> R.string.stretch - else -> throw IllegalArgumentException("Invalid fullscreen scaling mode: $fullscreenScaling") - } - return getString(resourceId) - } - - private fun scalingFilterToString(scalingFilter: Int): String { - val resourceId = when (scalingFilter) { - NativeSettings.SCALING_FILTER_BILINEAR_FILTER -> R.string.bilinear - NativeSettings.SCALING_FILTER_BICUBIC_FILTER -> R.string.bicubic - NativeSettings.SCALING_FILTER_BICUBIC_HERMITE_FILTER -> R.string.hermite - NativeSettings.SCALING_FILTER_NEAREST_NEIGHBOR_FILTER -> R.string.nearest_neighbor - else -> throw IllegalArgumentException("Invalid scaling filter: $scalingFilter") - } - return getString(resourceId) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val binding = LayoutGenericRecyclerViewBinding.inflate(inflater, container, false) - - val genericRecyclerViewAdapter = GenericRecyclerViewAdapter() - - val asyncShaderToggle = ToggleRecyclerViewItem( - getString(R.string.async_shader_compile), - getString(R.string.async_shader_compile_description), - asyncShaderCompile, - NativeSettings::asyncShaderCompile::set - ) - genericRecyclerViewAdapter.addRecyclerViewItem(asyncShaderToggle) - - val vsyncModeSelection = SingleSelectionRecyclerViewItem( - getString(R.string.vsync), - vSyncMode, - listOf( - NativeSettings.VSYNC_MODE_OFF, - NativeSettings.VSYNC_MODE_DOUBLE_BUFFERING, - NativeSettings.VSYNC_MODE_TRIPLE_BUFFERING - ), - { vsyncMode: Int -> this.vsyncModeToString(vsyncMode) }, - NativeSettings::vSyncMode::set - ) - genericRecyclerViewAdapter.addRecyclerViewItem(vsyncModeSelection) - - val accurateBarriersToggle = ToggleRecyclerViewItem( - getString(R.string.accurate_barriers), - getString(R.string.accurate_barriers_description), - accurateBarriers, - NativeSettings::accurateBarriers::set - ) - genericRecyclerViewAdapter.addRecyclerViewItem(accurateBarriersToggle) - - val fullscreenScalingSelection = SingleSelectionRecyclerViewItem( - getString(R.string.fullscreen_scaling), - fullscreenScaling, - listOf( - NativeSettings.FULLSCREEN_SCALING_KEEP_ASPECT_RATIO, - NativeSettings.FULLSCREEN_SCALING_STRETCH - ), - { fullscreenScaling: Int -> this.fullscreenScalingModeToString(fullscreenScaling) }, - NativeSettings::fullscreenScaling::set - ) - genericRecyclerViewAdapter.addRecyclerViewItem(fullscreenScalingSelection) - - val scalingFilterChoices = List.of( - NativeSettings.SCALING_FILTER_BILINEAR_FILTER, - NativeSettings.SCALING_FILTER_BICUBIC_FILTER, - NativeSettings.SCALING_FILTER_BICUBIC_HERMITE_FILTER, - NativeSettings.SCALING_FILTER_NEAREST_NEIGHBOR_FILTER - ) - - val upscaleFilterSelection = SingleSelectionRecyclerViewItem( - getString(R.string.upscale_filter), - upscalingFilter, - scalingFilterChoices, - { scalingFilter: Int -> this.scalingFilterToString(scalingFilter) }, - NativeSettings::upscalingFilter::set - ) - genericRecyclerViewAdapter.addRecyclerViewItem(upscaleFilterSelection) - - val downscaleFilterSelection = SingleSelectionRecyclerViewItem( - getString(R.string.downscale_filter), - downscalingFilter, - scalingFilterChoices, - { scalingFilter: Int -> this.scalingFilterToString(scalingFilter) }, - NativeSettings::downscalingFilter::set - ) - genericRecyclerViewAdapter.addRecyclerViewItem(downscaleFilterSelection) - - binding.recyclerView.adapter = genericRecyclerViewAdapter - return binding.root - } -} \ 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 new file mode 100644 index 00000000..65d4b226 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/graphics/GraphicsSettingsScreen.kt @@ -0,0 +1,76 @@ +package info.cemu.cemu.settings.graphics + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import info.cemu.cemu.R +import info.cemu.cemu.guicore.ScreenContent +import info.cemu.cemu.guicore.SingleSelection +import info.cemu.cemu.guicore.Toggle +import info.cemu.cemu.guicore.enumtostringmapper.native.fullscreenScalingModeToStringId +import info.cemu.cemu.guicore.enumtostringmapper.native.scalingFilterToStringId +import info.cemu.cemu.guicore.enumtostringmapper.native.vsyncModeToStringId +import info.cemu.cemu.nativeinterface.NativeSettings + + +val ScalingFilterChoices = listOf( + NativeSettings.SCALING_FILTER_BILINEAR_FILTER, + NativeSettings.SCALING_FILTER_BICUBIC_FILTER, + NativeSettings.SCALING_FILTER_BICUBIC_HERMITE_FILTER, + NativeSettings.SCALING_FILTER_NEAREST_NEIGHBOR_FILTER +) + +@Composable +fun GraphicsSettingsScreen(navigateBack: () -> Unit) { + ScreenContent( + appBarText = stringResource(R.string.general_settings), + navigateBack = navigateBack, + ) { + Toggle( + label = stringResource(R.string.async_shader_compile), + description = stringResource(R.string.async_shader_compile_description), + initialCheckedState = NativeSettings::getAsyncShaderCompile, + onCheckedChanged = NativeSettings::setAsyncShaderCompile, + ) + SingleSelection( + label = stringResource(R.string.vsync), + initialChoice = NativeSettings::getVsyncMode, + onChoiceChanged = NativeSettings::setVsyncMode, + choiceToString = { stringResource(vsyncModeToStringId(it)) }, + choices = listOf( + NativeSettings.VSYNC_MODE_OFF, + NativeSettings.VSYNC_MODE_DOUBLE_BUFFERING, + NativeSettings.VSYNC_MODE_TRIPLE_BUFFERING + ), + ) + Toggle( + label = stringResource(R.string.accurate_barriers), + description = stringResource(R.string.accurate_barriers_description), + initialCheckedState = NativeSettings::getAccurateBarriers, + onCheckedChanged = NativeSettings::setAccurateBarriers, + ) + SingleSelection( + label = stringResource(R.string.fullscreen_scaling), + initialChoice = NativeSettings::getFullscreenScaling, + onChoiceChanged = NativeSettings::setFullscreenScaling, + choiceToString = { stringResource(fullscreenScalingModeToStringId(it)) }, + choices = listOf( + NativeSettings.FULLSCREEN_SCALING_KEEP_ASPECT_RATIO, + NativeSettings.FULLSCREEN_SCALING_STRETCH + ), + ) + SingleSelection( + label = stringResource(R.string.upscale_filter), + initialChoice = NativeSettings::getUpscalingFilter, + onChoiceChanged = NativeSettings::setUpscalingFilter, + choiceToString = { stringResource(scalingFilterToStringId(it)) }, + choices = ScalingFilterChoices, + ) + SingleSelection( + label = stringResource(R.string.downscale_filter), + initialChoice = NativeSettings::getDownscalingFilter, + onChoiceChanged = NativeSettings::setDownscalingFilter, + choiceToString = { stringResource(scalingFilterToStringId(it)) }, + choices = ScalingFilterChoices, + ) + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..5112d8b4 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ClassicControllerInputs.kt @@ -0,0 +1,70 @@ +package info.cemu.cemu.settings.input + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import info.cemu.cemu.R +import info.cemu.cemu.guicore.enumtostringmapper.native.classicControllerButtonToStringId +import info.cemu.cemu.nativeinterface.NativeInput + +@Composable +fun ClassicControllerInputs( + onInputClick: (String, Int) -> Unit, + controlsMapping: Map, +) { + @Composable + fun InputItemsGroup( + groupName: String, + inputIds: List, + ) { + InputItemsGroup( + groupName = groupName, + inputIds = inputIds, + inputIdToString = { stringResource(classicControllerButtonToStringId(it)) }, + onInputClick = onInputClick, + controlsMapping = controlsMapping, + ) + } + InputItemsGroup( + groupName = stringResource(R.string.buttons), + inputIds = listOf( + NativeInput.CLASSIC_BUTTON_A, + NativeInput.CLASSIC_BUTTON_B, + NativeInput.CLASSIC_BUTTON_X, + NativeInput.CLASSIC_BUTTON_Y, + NativeInput.CLASSIC_BUTTON_L, + NativeInput.CLASSIC_BUTTON_R, + NativeInput.CLASSIC_BUTTON_ZL, + NativeInput.CLASSIC_BUTTON_ZR, + NativeInput.CLASSIC_BUTTON_PLUS, + NativeInput.CLASSIC_BUTTON_MINUS, + NativeInput.CLASSIC_BUTTON_HOME + ) + ) + InputItemsGroup( + groupName = stringResource(R.string.d_pad), + inputIds = listOf( + NativeInput.CLASSIC_BUTTON_UP, + NativeInput.CLASSIC_BUTTON_DOWN, + NativeInput.CLASSIC_BUTTON_LEFT, + NativeInput.CLASSIC_BUTTON_RIGHT + ) + ) + InputItemsGroup( + groupName = stringResource(R.string.left_axis), + inputIds = listOf( + NativeInput.CLASSIC_BUTTON_STICKL_UP, + NativeInput.CLASSIC_BUTTON_STICKL_DOWN, + NativeInput.CLASSIC_BUTTON_STICKL_LEFT, + NativeInput.CLASSIC_BUTTON_STICKL_RIGHT + ) + ) + InputItemsGroup( + groupName = stringResource(R.string.right_axis), + inputIds = listOf( + NativeInput.CLASSIC_BUTTON_STICKR_UP, + NativeInput.CLASSIC_BUTTON_STICKR_DOWN, + NativeInput.CLASSIC_BUTTON_STICKR_LEFT, + NativeInput.CLASSIC_BUTTON_STICKR_RIGHT + ) + ) +} 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 new file mode 100644 index 00000000..fcf66895 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerInputSettingsScreen.kt @@ -0,0 +1,128 @@ +package info.cemu.cemu.settings.input + +import android.content.Context +import android.view.KeyEvent +import android.view.MotionEvent +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.MutableCreationExtras +import androidx.lifecycle.viewmodel.compose.viewModel +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import info.cemu.cemu.R +import info.cemu.cemu.guicore.Header +import info.cemu.cemu.guicore.ScreenContent +import info.cemu.cemu.guicore.SingleSelection +import info.cemu.cemu.guicore.Toggle +import info.cemu.cemu.nativeinterface.NativeInput +import kotlinx.serialization.Serializable + +@Composable +fun ControllerInputSettingsScreen( + navigateBack: () -> Unit, + controllerIndex: Int, + controllersViewModel: ControllersViewModel = viewModel( + factory = ControllersViewModel.Factory, + extras = MutableCreationExtras().apply { + set(ControllersViewModel.CONTROLLER_INDEX_KEY, controllerIndex) + } + ) +) { + val context = LocalContext.current + val controllerType = controllersViewModel.controllerType + + fun onInputClick(buttonName: String, buttonId: Int) { + openInputDialog( + context = context, + buttonName = buttonName, + onClear = { controllersViewModel.clearButtonMapping(buttonId) }, + tryMapKeyEvent = { controllersViewModel.tryMapKeyEvent(it, buttonId) }, + tryMapMotionEvent = { controllersViewModel.tryMapMotionEvent(it, buttonId) }, + ) + } + + ScreenContent( + appBarText = stringResource(R.string.controller_numbered, controllerIndex + 1), + navigateBack = navigateBack, + ) { + SingleSelection( + isChoiceEnabled = controllersViewModel::isControllerTypeAllowed, + label = stringResource(R.string.emulated_controller), + initialChoice = { controllersViewModel.controllerType }, + choices = listOf( + NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED, + NativeInput.EMULATED_CONTROLLER_TYPE_VPAD, + NativeInput.EMULATED_CONTROLLER_TYPE_PRO, + NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC, + NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE + ), + choiceToString = { stringResource(controllerTypeToStringId(it)) }, + onChoiceChanged = controllersViewModel::setControllerType + ) + when (controllerType) { + NativeInput.EMULATED_CONTROLLER_TYPE_VPAD -> VPADInputs( + controllerIndex = controllerIndex, + onInputClick = ::onInputClick, + controlsMapping = controllersViewModel.controls, + ) + + NativeInput.EMULATED_CONTROLLER_TYPE_PRO -> ProControllerInputs( + onInputClick = ::onInputClick, + controlsMapping = controllersViewModel.controls, + ) + + NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC -> ClassicControllerInputs( + onInputClick = ::onInputClick, + controlsMapping = controllersViewModel.controls, + ) + + NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE -> WiimoteControllerInputs( + onInputClick = ::onInputClick, + controlsMapping = controllersViewModel.controls, + ) + } + } +} + +fun openInputDialog( + context: Context, + buttonName: String, + onClear: () -> Unit, + tryMapKeyEvent: (KeyEvent) -> Boolean, + tryMapMotionEvent: (MotionEvent) -> Boolean, +) { + MaterialAlertDialogBuilder(context).setTitle(R.string.inputBindingDialogTitle) + .setMessage(context.getString(R.string.inputBindingDialogMessage, buttonName)) + .setNeutralButton(context.getString(R.string.clear)) { _, _ -> onClear() } + .setNegativeButton(context.getString(R.string.cancel)) { _, _ -> } + .show() + .also { alertDialog -> + alertDialog.requireViewById(android.R.id.message).apply { + isFocusableInTouchMode = true + requestFocus() + setOnKeyListener { _, _, keyEvent: KeyEvent -> + if (tryMapKeyEvent(keyEvent)) { + alertDialog.dismiss() + } + true + } + setOnGenericMotionListener { _, motionEvent: MotionEvent? -> + if (motionEvent != null && tryMapMotionEvent(motionEvent)) { + alertDialog.dismiss() + } + true + } + } + } +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerInputsFragment.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerInputsFragment.kt deleted file mode 100644 index e86b6ae3..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerInputsFragment.kt +++ /dev/null @@ -1,524 +0,0 @@ -package info.cemu.cemu.settings.input - -import android.content.DialogInterface -import android.os.Bundle -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.annotation.StringRes -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import info.cemu.cemu.R -import info.cemu.cemu.databinding.LayoutGenericRecyclerViewBinding -import info.cemu.cemu.guibasecomponents.GenericRecyclerViewAdapter -import info.cemu.cemu.guibasecomponents.HeaderRecyclerViewItem -import info.cemu.cemu.guibasecomponents.SingleSelectionRecyclerViewItem -import info.cemu.cemu.guibasecomponents.ToggleRecyclerViewItem -import info.cemu.cemu.input.InputManager -import info.cemu.cemu.nativeinterface.NativeInput -import info.cemu.cemu.nativeinterface.NativeInput.VPADControllersCount -import info.cemu.cemu.nativeinterface.NativeInput.WPADControllersCount -import info.cemu.cemu.nativeinterface.NativeInput.clearControllerMapping -import info.cemu.cemu.nativeinterface.NativeInput.getControllerMapping -import info.cemu.cemu.nativeinterface.NativeInput.getControllerMappings -import info.cemu.cemu.nativeinterface.NativeInput.getControllerType -import info.cemu.cemu.nativeinterface.NativeInput.getVPADScreenToggle -import info.cemu.cemu.nativeinterface.NativeInput.isControllerDisabled -import info.cemu.cemu.nativeinterface.NativeInput.setControllerType -import info.cemu.cemu.nativeinterface.NativeInput.setVPADScreenToggle -import info.cemu.cemu.settings.input.ControllerTypeResourceNameMapper.controllerTypeToResourceNameId -import java.util.function.Function - -class ControllerInputsFragment : Fragment() { - private var controllerIndex = 0 - private var controllerType = NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED - private val inputManager = InputManager() - private val genericRecyclerViewAdapter = GenericRecyclerViewAdapter() - private val emulatedControllerTypeAdapter = EmulatedControllerTypeAdapter() - private var buttonIdToStringResourceIdFunction: ((Int) -> Int)? = null - - private fun onTypeChanged(controllerType: Int) { - if (this.controllerType == controllerType) { - return - } - this.controllerType = controllerType - setControllerType(controllerIndex, controllerType) - genericRecyclerViewAdapter.clearRecyclerViewItems() - setControllerInputs() - } - - private fun addHeader(@StringRes resId: Int) { - genericRecyclerViewAdapter.addRecyclerViewItem(HeaderRecyclerViewItem(resId)) - } - - private fun onInputPressed( - inputItem: InputRecyclerViewItem, - buttonId: Int, - @StringRes buttonResourceIdName: Int - ) { - val inputDialog = - MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.inputBindingDialogTitle) - .setMessage( - getString( - R.string.inputBindingDialogMessage, - getString(buttonResourceIdName) - ) - ) - .setNeutralButton(getString(R.string.clear)) { dialogInterface: DialogInterface, i: Int -> - clearControllerMapping(controllerIndex, buttonId) - inputItem.clearBoundInput() - dialogInterface.dismiss() - } - .setNegativeButton( - getString(R.string.cancel) - ) { dialogInterface: DialogInterface, i: Int -> dialogInterface.dismiss() } - .show() - val messageTextView = inputDialog.requireViewById(android.R.id.message) - messageTextView.isFocusableInTouchMode = true - messageTextView.requestFocus() - messageTextView.setOnKeyListener { v: View?, keyCode: Int, event: KeyEvent? -> - if (inputManager.mapKeyEventToMappingId( - controllerIndex, buttonId, - event!! - ) - ) { - inputItem.setBoundInput(getControllerMapping(controllerIndex, buttonId)) - inputDialog.dismiss() - } - true - } - messageTextView.setOnGenericMotionListener { v: View?, event: MotionEvent? -> - if (inputManager.mapMotionEventToMappingId( - controllerIndex, buttonId, - event!! - ) - ) { - inputItem.setBoundInput(getControllerMapping(controllerIndex, buttonId)) - inputDialog.dismiss() - } - true - } - } - - private fun addInput(buttonId: Int, @StringRes buttonResourceIdName: Int, boundInput: String?) { - val inputRecyclerViewItem = InputRecyclerViewItem( - buttonResourceIdName, - boundInput - ) { inputItem: InputRecyclerViewItem -> - onInputPressed( - inputItem, - buttonId, - buttonResourceIdName - ) - } - genericRecyclerViewAdapter.addRecyclerViewItem(inputRecyclerViewItem) - } - - fun addControllerInputsGroup( - @StringRes groupTextResourceId: Int, - buttonIds: List, - boundInputsMap: Map - ) { - addHeader(groupTextResourceId) - for (buttonId in buttonIds) { - addInput( - buttonId, - buttonIdToStringResourceIdFunction!!(buttonId), - boundInputsMap.getOrDefault(buttonId, "") - ) - } - } - - private fun setControllerInputs() { - emulatedControllerTypeAdapter.setSelectedValue(controllerType) - emulatedControllerTypeAdapter.setControllerTypeCounts( - VPADControllersCount, - WPADControllersCount - ) - val controllerTypeName = getString(controllerTypeToResourceNameId(controllerType)) - buttonIdToStringResourceIdFunction = getButtonIdToStringResIdFn(controllerType) - - val emulatedControllerSelection = SingleSelectionRecyclerViewItem( - getString(R.string.emulated_controller_label), - getString(R.string.emulated_controller_selection_description, controllerTypeName), - emulatedControllerTypeAdapter - ) { controllerType: Int, selectionRecyclerViewItem: SingleSelectionRecyclerViewItem? -> - onTypeChanged( - controllerType - ) - } - genericRecyclerViewAdapter.addRecyclerViewItem(emulatedControllerSelection) - when (controllerType) { - NativeInput.EMULATED_CONTROLLER_TYPE_VPAD -> configureVPADInputs() - NativeInput.EMULATED_CONTROLLER_TYPE_PRO -> configureProInputs() - NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC -> configureClassicInputs() - NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE -> configureWiimoteInputs() - } - } - - fun configureVPADInputs() { - val boundInputsMap = getControllerMappings(controllerIndex) - addControllerInputsGroup( - R.string.buttons, - java.util.List.of( - NativeInput.VPAD_BUTTON_A, - NativeInput.VPAD_BUTTON_B, - NativeInput.VPAD_BUTTON_X, - NativeInput.VPAD_BUTTON_Y, - NativeInput.VPAD_BUTTON_L, - NativeInput.VPAD_BUTTON_R, - NativeInput.VPAD_BUTTON_ZL, - NativeInput.VPAD_BUTTON_ZR, - NativeInput.VPAD_BUTTON_PLUS, - NativeInput.VPAD_BUTTON_MINUS - ), - boundInputsMap!! - ) - addControllerInputsGroup( - R.string.d_pad, - java.util.List.of( - NativeInput.VPAD_BUTTON_UP, - NativeInput.VPAD_BUTTON_DOWN, - NativeInput.VPAD_BUTTON_LEFT, - NativeInput.VPAD_BUTTON_RIGHT - ), - boundInputsMap - ) - addControllerInputsGroup( - R.string.left_axis, - java.util.List.of( - NativeInput.VPAD_BUTTON_STICKL, - NativeInput.VPAD_BUTTON_STICKL_UP, - NativeInput.VPAD_BUTTON_STICKL_DOWN, - NativeInput.VPAD_BUTTON_STICKL_LEFT, - NativeInput.VPAD_BUTTON_STICKL_RIGHT - ), - boundInputsMap - ) - addControllerInputsGroup( - R.string.right_axis, - java.util.List.of( - NativeInput.VPAD_BUTTON_STICKR, - NativeInput.VPAD_BUTTON_STICKR_UP, - NativeInput.VPAD_BUTTON_STICKR_DOWN, - NativeInput.VPAD_BUTTON_STICKR_LEFT, - NativeInput.VPAD_BUTTON_STICKR_RIGHT - ), - boundInputsMap - ) - addControllerInputsGroup( - R.string.extra, - java.util.List.of( - NativeInput.VPAD_BUTTON_MIC, - NativeInput.VPAD_BUTTON_HOME, - NativeInput.VPAD_BUTTON_SCREEN - ), - boundInputsMap - ) - val toggleShowScreen = ToggleRecyclerViewItem( - getString(R.string.toggle_screen), - getString(R.string.toggle_screen_description), - getVPADScreenToggle(controllerIndex) - ) { toggle: Boolean -> setVPADScreenToggle(controllerIndex, toggle) } - genericRecyclerViewAdapter.addRecyclerViewItem(toggleShowScreen) - } - - fun configureProInputs() { - val boundInputsMap = getControllerMappings(controllerIndex) - addControllerInputsGroup( - R.string.buttons, - java.util.List.of( - NativeInput.PRO_BUTTON_A, - NativeInput.PRO_BUTTON_B, - NativeInput.PRO_BUTTON_X, - NativeInput.PRO_BUTTON_Y, - NativeInput.PRO_BUTTON_L, - NativeInput.PRO_BUTTON_R, - NativeInput.PRO_BUTTON_ZL, - NativeInput.PRO_BUTTON_ZR, - NativeInput.PRO_BUTTON_PLUS, - NativeInput.PRO_BUTTON_MINUS, - NativeInput.PRO_BUTTON_HOME - ), - boundInputsMap!! - ) - addControllerInputsGroup( - R.string.left_axis, - java.util.List.of( - NativeInput.PRO_BUTTON_STICKL, - NativeInput.PRO_BUTTON_STICKL_UP, - NativeInput.PRO_BUTTON_STICKL_DOWN, - NativeInput.PRO_BUTTON_STICKL_LEFT, - NativeInput.PRO_BUTTON_STICKL_RIGHT - ), - boundInputsMap - ) - addControllerInputsGroup( - R.string.right_axis, - java.util.List.of( - NativeInput.PRO_BUTTON_STICKR, - NativeInput.PRO_BUTTON_STICKR_UP, - NativeInput.PRO_BUTTON_STICKR_DOWN, - NativeInput.PRO_BUTTON_STICKR_LEFT, - NativeInput.PRO_BUTTON_STICKR_RIGHT - ), - boundInputsMap - ) - addControllerInputsGroup( - R.string.d_pad, - java.util.List.of( - NativeInput.PRO_BUTTON_UP, - NativeInput.PRO_BUTTON_DOWN, - NativeInput.PRO_BUTTON_LEFT, - NativeInput.PRO_BUTTON_RIGHT - ), - boundInputsMap - ) - } - - fun configureClassicInputs() { - val boundInputsMap = getControllerMappings(controllerIndex) - addControllerInputsGroup( - R.string.buttons, - java.util.List.of( - NativeInput.CLASSIC_BUTTON_A, - NativeInput.CLASSIC_BUTTON_B, - NativeInput.CLASSIC_BUTTON_X, - NativeInput.CLASSIC_BUTTON_Y, - NativeInput.CLASSIC_BUTTON_L, - NativeInput.CLASSIC_BUTTON_R, - NativeInput.CLASSIC_BUTTON_ZL, - NativeInput.CLASSIC_BUTTON_ZR, - NativeInput.CLASSIC_BUTTON_PLUS, - NativeInput.CLASSIC_BUTTON_MINUS, - NativeInput.CLASSIC_BUTTON_HOME - ), - boundInputsMap!! - ) - addControllerInputsGroup( - R.string.left_axis, - java.util.List.of( - NativeInput.CLASSIC_BUTTON_STICKL_UP, - NativeInput.CLASSIC_BUTTON_STICKL_DOWN, - NativeInput.CLASSIC_BUTTON_STICKL_LEFT, - NativeInput.CLASSIC_BUTTON_STICKL_RIGHT - ), - boundInputsMap - ) - addControllerInputsGroup( - R.string.right_axis, - java.util.List.of( - NativeInput.CLASSIC_BUTTON_STICKR_UP, - NativeInput.CLASSIC_BUTTON_STICKR_DOWN, - NativeInput.CLASSIC_BUTTON_STICKR_LEFT, - NativeInput.CLASSIC_BUTTON_STICKR_RIGHT - ), - boundInputsMap - ) - addControllerInputsGroup( - R.string.d_pad, - java.util.List.of( - NativeInput.CLASSIC_BUTTON_UP, - NativeInput.CLASSIC_BUTTON_DOWN, - NativeInput.CLASSIC_BUTTON_LEFT, - NativeInput.CLASSIC_BUTTON_RIGHT - ), - boundInputsMap - ) - } - - fun configureWiimoteInputs() { - val boundInputsMap = getControllerMappings(controllerIndex) - addControllerInputsGroup( - R.string.buttons, - java.util.List.of( - NativeInput.WIIMOTE_BUTTON_A, - NativeInput.WIIMOTE_BUTTON_B, - NativeInput.WIIMOTE_BUTTON_1, - NativeInput.WIIMOTE_BUTTON_2, - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_Z, - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_C, - NativeInput.WIIMOTE_BUTTON_PLUS, - NativeInput.WIIMOTE_BUTTON_MINUS, - NativeInput.WIIMOTE_BUTTON_HOME - ), - boundInputsMap!! - ) - addControllerInputsGroup( - R.string.d_pad, - java.util.List.of( - NativeInput.WIIMOTE_BUTTON_UP, - NativeInput.WIIMOTE_BUTTON_DOWN, - NativeInput.WIIMOTE_BUTTON_LEFT, - NativeInput.WIIMOTE_BUTTON_RIGHT - ), - boundInputsMap - ) - addControllerInputsGroup( - R.string.nunchuck, - java.util.List.of( - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_UP, - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_DOWN, - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_LEFT, - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_RIGHT - ), - boundInputsMap - ) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - super.onCreate(savedInstanceState) - controllerIndex = requireArguments().getInt(CONTROLLER_INDEX) - val actionBar = (requireActivity() as AppCompatActivity).supportActionBar - if (actionBar != null) { - actionBar.title = getString(R.string.controller_numbered, controllerIndex + 1) - } - if (!isControllerDisabled(controllerIndex)) { - controllerType = getControllerType(controllerIndex) - } - setControllerInputs() - - val binding = LayoutGenericRecyclerViewBinding.inflate(inflater, container, false) - binding.recyclerView.adapter = genericRecyclerViewAdapter - return binding.root - } - - private fun getButtonIdToStringResIdFn(controllerType: Int): ((Int) -> Int)? { - return when (controllerType) { - NativeInput.EMULATED_CONTROLLER_TYPE_VPAD -> ::getButtonVPADResourceIdName - NativeInput.EMULATED_CONTROLLER_TYPE_PRO -> ::getButtonProControllerResourceIdName - NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC -> ::getButtonClassicControllerResourceIdName - NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE -> ::getButtonWiimoteResourceIdName - NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED -> null - else -> throw IllegalArgumentException("Invalid controllerType $controllerType") - } - } - - private fun getButtonVPADResourceIdName(buttonId: Int): Int { - return when (buttonId) { - NativeInput.VPAD_BUTTON_A -> R.string.button_a - NativeInput.VPAD_BUTTON_B -> R.string.button_b - NativeInput.VPAD_BUTTON_X -> R.string.button_x - NativeInput.VPAD_BUTTON_Y -> R.string.button_y - NativeInput.VPAD_BUTTON_L -> R.string.button_l - NativeInput.VPAD_BUTTON_R -> R.string.button_r - NativeInput.VPAD_BUTTON_ZL -> R.string.button_zl - NativeInput.VPAD_BUTTON_ZR -> R.string.button_zr - NativeInput.VPAD_BUTTON_PLUS -> R.string.button_plus - NativeInput.VPAD_BUTTON_MINUS -> R.string.button_minus - NativeInput.VPAD_BUTTON_UP -> R.string.button_up - NativeInput.VPAD_BUTTON_DOWN -> R.string.button_down - NativeInput.VPAD_BUTTON_LEFT -> R.string.button_left - NativeInput.VPAD_BUTTON_RIGHT -> R.string.button_right - NativeInput.VPAD_BUTTON_STICKL -> R.string.button_stickl - NativeInput.VPAD_BUTTON_STICKR -> R.string.button_stickr - NativeInput.VPAD_BUTTON_STICKL_UP -> R.string.button_stickl_up - NativeInput.VPAD_BUTTON_STICKL_DOWN -> R.string.button_stickl_down - NativeInput.VPAD_BUTTON_STICKL_LEFT -> R.string.button_stickl_left - NativeInput.VPAD_BUTTON_STICKL_RIGHT -> R.string.button_stickl_right - NativeInput.VPAD_BUTTON_STICKR_UP -> R.string.button_stickr_up - NativeInput.VPAD_BUTTON_STICKR_DOWN -> R.string.button_stickr_down - NativeInput.VPAD_BUTTON_STICKR_LEFT -> R.string.button_stickr_left - NativeInput.VPAD_BUTTON_STICKR_RIGHT -> R.string.button_stickr_right - NativeInput.VPAD_BUTTON_MIC -> R.string.button_mic - NativeInput.VPAD_BUTTON_SCREEN -> R.string.button_screen - NativeInput.VPAD_BUTTON_HOME -> R.string.button_home - else -> throw IllegalArgumentException("Invalid buttonId $buttonId for VPAD controller type") - } - } - - private fun getButtonProControllerResourceIdName(buttonId: Int): Int { - return when (buttonId) { - NativeInput.PRO_BUTTON_A -> R.string.button_a - NativeInput.PRO_BUTTON_B -> R.string.button_b - NativeInput.PRO_BUTTON_X -> R.string.button_x - NativeInput.PRO_BUTTON_Y -> R.string.button_y - NativeInput.PRO_BUTTON_L -> R.string.button_l - NativeInput.PRO_BUTTON_R -> R.string.button_r - NativeInput.PRO_BUTTON_ZL -> R.string.button_zl - NativeInput.PRO_BUTTON_ZR -> R.string.button_zr - NativeInput.PRO_BUTTON_PLUS -> R.string.button_plus - NativeInput.PRO_BUTTON_MINUS -> R.string.button_minus - NativeInput.PRO_BUTTON_HOME -> R.string.button_home - NativeInput.PRO_BUTTON_UP -> R.string.button_up - NativeInput.PRO_BUTTON_DOWN -> R.string.button_down - NativeInput.PRO_BUTTON_LEFT -> R.string.button_left - NativeInput.PRO_BUTTON_RIGHT -> R.string.button_right - NativeInput.PRO_BUTTON_STICKL -> R.string.button_stickl - NativeInput.PRO_BUTTON_STICKR -> R.string.button_stickr - NativeInput.PRO_BUTTON_STICKL_UP -> R.string.button_stickl_up - NativeInput.PRO_BUTTON_STICKL_DOWN -> R.string.button_stickl_down - NativeInput.PRO_BUTTON_STICKL_LEFT -> R.string.button_stickl_left - NativeInput.PRO_BUTTON_STICKL_RIGHT -> R.string.button_stickl_right - NativeInput.PRO_BUTTON_STICKR_UP -> R.string.button_stickr_up - NativeInput.PRO_BUTTON_STICKR_DOWN -> R.string.button_stickr_down - NativeInput.PRO_BUTTON_STICKR_LEFT -> R.string.button_stickr_left - NativeInput.PRO_BUTTON_STICKR_RIGHT -> R.string.button_stickr_right - else -> throw IllegalArgumentException("Invalid buttonId $buttonId for Pro controller type") - } - } - - private fun getButtonClassicControllerResourceIdName(buttonId: Int): Int { - return when (buttonId) { - NativeInput.CLASSIC_BUTTON_A -> R.string.button_a - NativeInput.CLASSIC_BUTTON_B -> R.string.button_b - NativeInput.CLASSIC_BUTTON_X -> R.string.button_x - NativeInput.CLASSIC_BUTTON_Y -> R.string.button_y - NativeInput.CLASSIC_BUTTON_L -> R.string.button_l - NativeInput.CLASSIC_BUTTON_R -> R.string.button_r - NativeInput.CLASSIC_BUTTON_ZL -> R.string.button_zl - NativeInput.CLASSIC_BUTTON_ZR -> R.string.button_zr - NativeInput.CLASSIC_BUTTON_PLUS -> R.string.button_plus - NativeInput.CLASSIC_BUTTON_MINUS -> R.string.button_minus - NativeInput.CLASSIC_BUTTON_HOME -> R.string.button_home - NativeInput.CLASSIC_BUTTON_UP -> R.string.button_up - NativeInput.CLASSIC_BUTTON_DOWN -> R.string.button_down - NativeInput.CLASSIC_BUTTON_LEFT -> R.string.button_left - NativeInput.CLASSIC_BUTTON_RIGHT -> R.string.button_right - NativeInput.CLASSIC_BUTTON_STICKL_UP -> R.string.button_stickl_up - NativeInput.CLASSIC_BUTTON_STICKL_DOWN -> R.string.button_stickl_down - NativeInput.CLASSIC_BUTTON_STICKL_LEFT -> R.string.button_stickl_left - NativeInput.CLASSIC_BUTTON_STICKL_RIGHT -> R.string.button_stickl_right - NativeInput.CLASSIC_BUTTON_STICKR_UP -> R.string.button_stickr_up - NativeInput.CLASSIC_BUTTON_STICKR_DOWN -> R.string.button_stickr_down - NativeInput.CLASSIC_BUTTON_STICKR_LEFT -> R.string.button_stickr_left - NativeInput.CLASSIC_BUTTON_STICKR_RIGHT -> R.string.button_stickr_right - else -> throw IllegalArgumentException("Invalid buttonId $buttonId for Classic controller type") - } - } - - private fun getButtonWiimoteResourceIdName(buttonId: Int): Int { - return when (buttonId) { - NativeInput.WIIMOTE_BUTTON_A -> R.string.button_a - NativeInput.WIIMOTE_BUTTON_B -> R.string.button_b - NativeInput.WIIMOTE_BUTTON_1 -> R.string.button_1 - NativeInput.WIIMOTE_BUTTON_2 -> R.string.button_2 - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_Z -> R.string.button_nunchuck_z - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_C -> R.string.button_nunchuck_c - NativeInput.WIIMOTE_BUTTON_PLUS -> R.string.button_plus - NativeInput.WIIMOTE_BUTTON_MINUS -> R.string.button_minus - NativeInput.WIIMOTE_BUTTON_UP -> R.string.button_up - NativeInput.WIIMOTE_BUTTON_DOWN -> R.string.button_down - NativeInput.WIIMOTE_BUTTON_LEFT -> R.string.button_left - NativeInput.WIIMOTE_BUTTON_RIGHT -> R.string.button_right - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_UP -> R.string.button_nunchuck_up - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_DOWN -> R.string.button_nunchuck_down - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_LEFT -> R.string.button_nunchuck_left - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_RIGHT -> R.string.button_nunchuck_right - NativeInput.WIIMOTE_BUTTON_HOME -> R.string.button_home - else -> throw IllegalArgumentException("Invalid buttonId $buttonId for Wiimote controller type") - } - } - - companion object { - const val CONTROLLER_INDEX: String = "ControllerIndex" - } -} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerTypeResourceNameMapper.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerTypeResourceNameMapper.kt index cb791636..b1210322 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerTypeResourceNameMapper.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerTypeResourceNameMapper.kt @@ -1,18 +1,15 @@ package info.cemu.cemu.settings.input +import androidx.annotation.StringRes import info.cemu.cemu.R import info.cemu.cemu.nativeinterface.NativeInput -object ControllerTypeResourceNameMapper { - @JvmStatic - fun controllerTypeToResourceNameId(type: Int): Int { - return when (type) { - NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED -> R.string.disabled - NativeInput.EMULATED_CONTROLLER_TYPE_VPAD -> R.string.vpad_controller - NativeInput.EMULATED_CONTROLLER_TYPE_PRO -> R.string.pro_controller - NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE -> R.string.wiimote_controller - NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC -> R.string.classic_controller - else -> throw IllegalArgumentException("Invalid controller type: $type") - } - } +@StringRes +fun controllerTypeToStringId(type: Int) = when (type) { + NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED -> R.string.disabled + NativeInput.EMULATED_CONTROLLER_TYPE_VPAD -> R.string.vpad_controller + NativeInput.EMULATED_CONTROLLER_TYPE_PRO -> R.string.pro_controller + NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE -> R.string.wiimote_controller + NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC -> R.string.classic_controller + else -> throw IllegalArgumentException("Invalid controller type: $type") } diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllersViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllersViewModel.kt new file mode 100644 index 00000000..f96c09cf --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllersViewModel.kt @@ -0,0 +1,97 @@ +package info.cemu.cemu.settings.input + +import android.view.KeyEvent +import android.view.MotionEvent +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateMapOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import info.cemu.cemu.input.InputManager +import info.cemu.cemu.nativeinterface.NativeInput + +class ControllersViewModel(val controllerIndex: Int) : ViewModel() { + private val inputManager = InputManager() + private var _controllerType = mutableIntStateOf( + if (NativeInput.isControllerDisabled(controllerIndex)) NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED + else NativeInput.getControllerType(controllerIndex) + ) + val controllerType: Int + get() = _controllerType.intValue + + private var _controls = mutableStateMapOf() + val controls: Map + get() = _controls + + private var vpadCount = 0 + private var wpadCount = 0 + + + fun setControllerType(controllerType: Int) { + if (!isControllerTypeAllowed(controllerType) || _controllerType.intValue == controllerType) + return + _controllerType.intValue = controllerType + NativeInput.setControllerType(controllerIndex, controllerType) + refreshControllerData() + } + + fun tryMapKeyEvent(keyEvent: KeyEvent, buttonId: Int): Boolean { + if (inputManager.mapKeyEventToMappingId(controllerIndex, buttonId, keyEvent)) { + _controls[buttonId] = NativeInput.getControllerMapping(controllerIndex, buttonId) + return true + } + return false + } + + fun tryMapMotionEvent(motionEvent: MotionEvent, buttonId: Int): Boolean { + if (inputManager.mapMotionEventToMappingId(controllerIndex, buttonId, motionEvent)) { + _controls[buttonId] = NativeInput.getControllerMapping(controllerIndex, buttonId) + return true + } + return false + } + + fun clearButtonMapping(buttonId: Int) { + _controls.remove(buttonId) + NativeInput.clearControllerMapping(controllerIndex, buttonId) + } + + private fun refreshControllerData() { + vpadCount = NativeInput.VPADControllersCount + wpadCount = NativeInput.WPADControllersCount + _controls.apply { + clear() + putAll(NativeInput.getControllerMappings(controllerIndex)) + } + } + + fun isControllerTypeAllowed(controllerType: Int): Boolean { + val currentControllerType = this.controllerType + if (controllerType == NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED) { + return true + } + if (controllerType == NativeInput.EMULATED_CONTROLLER_TYPE_VPAD) { + return this.controllerType == NativeInput.EMULATED_CONTROLLER_TYPE_VPAD || vpadCount < NativeInput.MAX_VPAD_CONTROLLERS + } + val isWPAD = currentControllerType != NativeInput.EMULATED_CONTROLLER_TYPE_VPAD + && currentControllerType != NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED + return isWPAD || wpadCount < NativeInput.MAX_WPAD_CONTROLLERS + } + + init { + refreshControllerData() + } + + companion object { + val CONTROLLER_INDEX_KEY = object : CreationExtras.Key {} + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + ControllersViewModel( + this[CONTROLLER_INDEX_KEY] as Int + ) + } + } + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/EmulatedControllerTypeAdapter.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/EmulatedControllerTypeAdapter.kt deleted file mode 100644 index 52c8cb64..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/EmulatedControllerTypeAdapter.kt +++ /dev/null @@ -1,51 +0,0 @@ -package info.cemu.cemu.settings.input - -import android.widget.TextView -import info.cemu.cemu.guibasecomponents.SelectionAdapter -import info.cemu.cemu.nativeinterface.NativeInput -import info.cemu.cemu.settings.input.ControllerTypeResourceNameMapper.controllerTypeToResourceNameId -import java.util.stream.Collectors -import java.util.stream.Stream - - -class EmulatedControllerTypeAdapter : SelectionAdapter() { - private var vpadCount = 0 - private var wpadCount = 0 - - init { - choiceItems = listOf( - NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED, - NativeInput.EMULATED_CONTROLLER_TYPE_VPAD, - NativeInput.EMULATED_CONTROLLER_TYPE_PRO, - NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC, - NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE - ).map { type: Int -> - ChoiceItem({ t: TextView -> - t.setText( - controllerTypeToResourceNameId(type) - ) - }, type) - }.toList() - setSelectedValue(NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED) - } - - fun setControllerTypeCounts(vpadCount: Int, wpadCount: Int) { - this.vpadCount = vpadCount - this.wpadCount = wpadCount - notifyDataSetChanged() - } - - override fun isEnabled(position: Int): Boolean { - val currentControllerType = getItem(selectedPosition)!! - val type = getItem(position)!! - if (type == NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED) { - return true - } - if (type == NativeInput.EMULATED_CONTROLLER_TYPE_VPAD) { - return currentControllerType == NativeInput.EMULATED_CONTROLLER_TYPE_VPAD || vpadCount < NativeInput.MAX_VPAD_CONTROLLERS - } - val isWPAD = - currentControllerType != NativeInput.EMULATED_CONTROLLER_TYPE_VPAD && currentControllerType != NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED - return isWPAD || wpadCount < NativeInput.MAX_WPAD_CONTROLLERS - } -} 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 new file mode 100644 index 00000000..47857fb4 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputItemGroup.kt @@ -0,0 +1,56 @@ +package info.cemu.cemu.settings.input + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +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.guicore.Header + +@Composable +fun InputItemsGroup( + groupName: String, + inputIds: List, + inputIdToString: @Composable (Int) -> String, + onInputClick: (String, Int) -> Unit, + controlsMapping: Map, +) { + Header(groupName) + inputIds.forEach { + val buttonName = inputIdToString(it) + InputItem( + buttonName = buttonName, + mapping = controlsMapping[it], + onClick = { onInputClick(buttonName, it) } + ) + } +} + +@Composable +fun InputItem( + buttonName: String, + mapping: String?, + onClick: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = buttonName, + fontSize = 18.sp, + ) + Text( + text = mapping ?: "", + fontSize = 16.sp + ) + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputRecyclerViewItem.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputRecyclerViewItem.kt deleted file mode 100644 index 24a6d25b..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputRecyclerViewItem.kt +++ /dev/null @@ -1,59 +0,0 @@ -package info.cemu.cemu.settings.input - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import info.cemu.cemu.R -import info.cemu.cemu.guibasecomponents.RecyclerViewItem - -class InputRecyclerViewItem( - private val inputNameResourceId: Int, - private var boundInput: String, - private val onInputClickListener: OnInputClickListener -) : - RecyclerViewItem { - interface OnInputClickListener { - fun onInputClick(inputRecyclerViewItem: InputRecyclerViewItem?) - } - - private class InputViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var inputName: TextView = - itemView.findViewById(R.id.controller_input_name) - var boundInput: TextView = - itemView.findViewById(R.id.controller_button_name) - } - - private var recyclerViewAdapter: RecyclerView.Adapter? = null - private var inputViewHolder: InputViewHolder? = null - - fun clearBoundInput() { - setBoundInput("") - } - - fun setBoundInput(boundInput: String) { - this.boundInput = boundInput - recyclerViewAdapter!!.notifyItemChanged(inputViewHolder!!.adapterPosition) - } - - override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { - return InputViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.layout_controller_input, parent, false) - ) - } - - override fun onBindViewHolder( - viewHolder: RecyclerView.ViewHolder, - adapter: RecyclerView.Adapter - ) { - inputViewHolder = viewHolder as InputViewHolder - recyclerViewAdapter = adapter - inputViewHolder!!.itemView.setOnClickListener { view: View? -> - onInputClickListener.onInputClick(this) - } - inputViewHolder!!.inputName.setText(inputNameResourceId) - inputViewHolder!!.boundInput.text = boundInput - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputSettingsFragment.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputSettingsFragment.kt deleted file mode 100644 index 414f3a7e..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputSettingsFragment.kt +++ /dev/null @@ -1,62 +0,0 @@ -package info.cemu.cemu.settings.input - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.NavHostFragment -import info.cemu.cemu.R -import info.cemu.cemu.databinding.LayoutGenericRecyclerViewBinding -import info.cemu.cemu.guibasecomponents.ButtonRecyclerViewItem -import info.cemu.cemu.guibasecomponents.GenericRecyclerViewAdapter -import info.cemu.cemu.guibasecomponents.SimpleButtonRecyclerViewItem -import info.cemu.cemu.nativeinterface.NativeInput -import info.cemu.cemu.nativeinterface.NativeInput.getControllerType -import info.cemu.cemu.nativeinterface.NativeInput.isControllerDisabled -import info.cemu.cemu.settings.input.ControllerTypeResourceNameMapper.controllerTypeToResourceNameId - - -class InputSettingsFragment : Fragment() { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val binding = LayoutGenericRecyclerViewBinding.inflate(inflater, container, false) - val genericRecyclerViewAdapter = GenericRecyclerViewAdapter() - genericRecyclerViewAdapter.addRecyclerViewItem( - SimpleButtonRecyclerViewItem( - getString(R.string.input_overlay_settings) - ) { - NavHostFragment.findNavController( - this@InputSettingsFragment - ).navigate(R.id.action_inputSettingsFragment_to_inputOverlaySettingsFragment) - } - ) - - for (index in 0 until NativeInput.MAX_CONTROLLERS) { - val controllerIndex = index - val controllerType = - if (isControllerDisabled(controllerIndex)) NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED else getControllerType( - controllerIndex - ) - val controllerTypeName = getString(controllerTypeToResourceNameId(controllerType)) - val buttonRecyclerViewItem = ButtonRecyclerViewItem( - getString(R.string.controller_numbered, controllerIndex + 1), - getString(R.string.emulated_controller_with_type, controllerTypeName) - ) { - val bundle = Bundle() - bundle.putInt(ControllerInputsFragment.CONTROLLER_INDEX, controllerIndex) - NavHostFragment.findNavController(this@InputSettingsFragment) - .navigate( - R.id.action_inputSettingsFragment_to_controllerInputsFragment, - bundle - ) - } - genericRecyclerViewAdapter.addRecyclerViewItem(buttonRecyclerViewItem) - } - binding.recyclerView.adapter = genericRecyclerViewAdapter - return binding.root - } -} 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 new file mode 100644 index 00000000..49bc55c3 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputSettingsScreen.kt @@ -0,0 +1,55 @@ +package info.cemu.cemu.settings.input + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.dropUnlessResumed +import info.cemu.cemu.R +import info.cemu.cemu.guicore.Button +import info.cemu.cemu.guicore.ScreenContent +import info.cemu.cemu.nativeinterface.NativeInput +import kotlinx.serialization.Serializable + +data class InputSettingsScreenActions( + val goToInputOverlaySettings: () -> Unit, + val goToControllerSettings: (controllerIndex: Int) -> Unit, +) + +@Composable +fun InputSettingsScreen(navigateBack: () -> Unit, actions: InputSettingsScreenActions) { + val controllers by remember { + mutableStateOf(ControllerList) + } + ScreenContent( + appBarText = stringResource(R.string.overlay_settings), + navigateBack = navigateBack, + ) { + Button( + label = stringResource(R.string.input_overlay_settings), + onClick = dropUnlessResumed { actions.goToInputOverlaySettings() }, + ) + controllers.forEach { controllerTypePair -> + val (controllerIndex, controllerEmulatedType) = controllerTypePair + Button( + label = stringResource(R.string.controller_numbered, controllerIndex + 1), + description = stringResource( + R.string.emulated_controller_with_type, + stringResource(controllerTypeToStringId(controllerEmulatedType)) + ), + onClick = dropUnlessResumed { actions.goToControllerSettings(controllerIndex) }, + ) + } + } +} + +val ControllerList: List> + get() = (0.. + controllerIndex to getControllerType(controllerIndex) + } + +private fun getControllerType(index: Int): Int = + if (NativeInput.isControllerDisabled(index)) + NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED + else NativeInput.getControllerType(index) 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 new file mode 100644 index 00000000..b0d3304f --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ProControllerInputs.kt @@ -0,0 +1,72 @@ +package info.cemu.cemu.settings.input + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import info.cemu.cemu.R +import info.cemu.cemu.guicore.enumtostringmapper.native.proControllerButtonToStringId +import info.cemu.cemu.nativeinterface.NativeInput + +@Composable +fun ProControllerInputs( + onInputClick: (String, Int) -> Unit, + controlsMapping: Map, +) { + @Composable + fun InputItemsGroup( + groupName: String, + inputIds: List, + ) { + InputItemsGroup( + groupName = groupName, + inputIds = inputIds, + inputIdToString = { stringResource(proControllerButtonToStringId(it)) }, + onInputClick = onInputClick, + controlsMapping = controlsMapping, + ) + } + InputItemsGroup( + groupName = stringResource(R.string.buttons), + inputIds = listOf( + NativeInput.PRO_BUTTON_A, + NativeInput.PRO_BUTTON_B, + NativeInput.PRO_BUTTON_X, + NativeInput.PRO_BUTTON_Y, + NativeInput.PRO_BUTTON_L, + NativeInput.PRO_BUTTON_R, + NativeInput.PRO_BUTTON_ZL, + NativeInput.PRO_BUTTON_ZR, + NativeInput.PRO_BUTTON_PLUS, + NativeInput.PRO_BUTTON_MINUS, + NativeInput.PRO_BUTTON_HOME + ) + ) + InputItemsGroup( + groupName = stringResource(R.string.d_pad), + inputIds = listOf( + NativeInput.PRO_BUTTON_UP, + NativeInput.PRO_BUTTON_DOWN, + NativeInput.PRO_BUTTON_LEFT, + NativeInput.PRO_BUTTON_RIGHT + ) + ) + InputItemsGroup( + groupName = stringResource(R.string.left_axis), + inputIds = listOf( + NativeInput.PRO_BUTTON_STICKL, + NativeInput.PRO_BUTTON_STICKL_UP, + NativeInput.PRO_BUTTON_STICKL_DOWN, + NativeInput.PRO_BUTTON_STICKL_LEFT, + NativeInput.PRO_BUTTON_STICKL_RIGHT + ) + ) + InputItemsGroup( + groupName = stringResource(R.string.right_axis), + inputIds = listOf( + NativeInput.PRO_BUTTON_STICKR, + NativeInput.PRO_BUTTON_STICKR_UP, + NativeInput.PRO_BUTTON_STICKR_DOWN, + NativeInput.PRO_BUTTON_STICKR_LEFT, + NativeInput.PRO_BUTTON_STICKR_RIGHT + ) + ) +} 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 new file mode 100644 index 00000000..d17f7542 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/VPADInputs.kt @@ -0,0 +1,87 @@ +package info.cemu.cemu.settings.input + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import info.cemu.cemu.R +import info.cemu.cemu.guicore.Toggle +import info.cemu.cemu.guicore.enumtostringmapper.native.vpadButtonToStringId +import info.cemu.cemu.nativeinterface.NativeInput + +@Composable +fun VPADInputs( + controllerIndex: Int, + onInputClick: (String, Int) -> Unit, + controlsMapping: Map, +) { + @Composable + fun InputItemsGroup( + groupName: String, + inputIds: List, + ) { + InputItemsGroup( + groupName = groupName, + inputIds = inputIds, + inputIdToString = { stringResource(vpadButtonToStringId(it)) }, + onInputClick = onInputClick, + controlsMapping = controlsMapping, + ) + } + InputItemsGroup( + groupName = stringResource(R.string.buttons), + inputIds = listOf( + NativeInput.VPAD_BUTTON_A, + NativeInput.VPAD_BUTTON_B, + NativeInput.VPAD_BUTTON_X, + NativeInput.VPAD_BUTTON_Y, + NativeInput.VPAD_BUTTON_L, + NativeInput.VPAD_BUTTON_R, + NativeInput.VPAD_BUTTON_ZL, + NativeInput.VPAD_BUTTON_ZR, + NativeInput.VPAD_BUTTON_PLUS, + NativeInput.VPAD_BUTTON_MINUS + ) + ) + InputItemsGroup( + groupName = stringResource(R.string.d_pad), + inputIds = listOf( + NativeInput.VPAD_BUTTON_UP, + NativeInput.VPAD_BUTTON_DOWN, + NativeInput.VPAD_BUTTON_LEFT, + NativeInput.VPAD_BUTTON_RIGHT + ) + ) + InputItemsGroup( + groupName = stringResource(R.string.left_axis), + inputIds = listOf( + NativeInput.VPAD_BUTTON_STICKL, + NativeInput.VPAD_BUTTON_STICKL_UP, + NativeInput.VPAD_BUTTON_STICKL_DOWN, + NativeInput.VPAD_BUTTON_STICKL_LEFT, + NativeInput.VPAD_BUTTON_STICKL_RIGHT + ) + ) + InputItemsGroup( + groupName = stringResource(R.string.right_axis), + inputIds = listOf( + NativeInput.VPAD_BUTTON_STICKR, + NativeInput.VPAD_BUTTON_STICKR_UP, + NativeInput.VPAD_BUTTON_STICKR_DOWN, + NativeInput.VPAD_BUTTON_STICKR_LEFT, + NativeInput.VPAD_BUTTON_STICKR_RIGHT + ) + ) + InputItemsGroup( + groupName = stringResource(R.string.extra), + inputIds = listOf( + NativeInput.VPAD_BUTTON_MIC, + NativeInput.VPAD_BUTTON_HOME, + NativeInput.VPAD_BUTTON_SCREEN + ) + ) + Toggle( + label = stringResource(R.string.toggle_screen), + description = stringResource(R.string.toggle_screen_description), + initialCheckedState = { NativeInput.getVPADScreenToggle(controllerIndex) }, + onCheckedChanged = { NativeInput.setVPADScreenToggle(controllerIndex, it) } + ) +} 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 new file mode 100644 index 00000000..35cd8d4b --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/WiimoteControllerInputs.kt @@ -0,0 +1,59 @@ +package info.cemu.cemu.settings.input + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import info.cemu.cemu.R +import info.cemu.cemu.guicore.enumtostringmapper.native.wiimoteButtonItToStringId +import info.cemu.cemu.nativeinterface.NativeInput + +@Composable +fun WiimoteControllerInputs( + onInputClick: (String, Int) -> Unit, + controlsMapping: Map, +) { + @Composable + fun InputItemsGroup( + groupName: String, + inputIds: List, + ) { + InputItemsGroup( + groupName = groupName, + inputIds = inputIds, + inputIdToString = { stringResource(wiimoteButtonItToStringId(it)) }, + onInputClick = onInputClick, + controlsMapping = controlsMapping, + ) + } + InputItemsGroup( + groupName = stringResource(R.string.buttons), + inputIds = listOf( + NativeInput.WIIMOTE_BUTTON_A, + NativeInput.WIIMOTE_BUTTON_B, + NativeInput.WIIMOTE_BUTTON_1, + NativeInput.WIIMOTE_BUTTON_2, + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_Z, + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_C, + NativeInput.WIIMOTE_BUTTON_PLUS, + NativeInput.WIIMOTE_BUTTON_MINUS, + NativeInput.WIIMOTE_BUTTON_HOME + ) + ) + InputItemsGroup( + groupName = stringResource(R.string.nunchuck), + inputIds = listOf( + NativeInput.WIIMOTE_BUTTON_UP, + NativeInput.WIIMOTE_BUTTON_DOWN, + NativeInput.WIIMOTE_BUTTON_LEFT, + NativeInput.WIIMOTE_BUTTON_RIGHT + ) + ) + InputItemsGroup( + groupName = stringResource(R.string.right_axis), + inputIds = listOf( + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_UP, + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_DOWN, + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_LEFT, + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_RIGHT + ) + ) +} 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 new file mode 100644 index 00000000..3cc26d56 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/inputoverlay/InputOverlaySettingsScreen.kt @@ -0,0 +1,57 @@ +package info.cemu.cemu.settings.inputoverlay + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.viewmodel.compose.viewModel +import info.cemu.cemu.R +import info.cemu.cemu.guicore.ScreenContent +import info.cemu.cemu.guicore.SingleSelection +import info.cemu.cemu.guicore.Slider +import info.cemu.cemu.guicore.Toggle +import info.cemu.cemu.nativeinterface.NativeInput +import kotlinx.serialization.Serializable + +private val ControllerIndexChoices = (0.. Unit +) { + val overlaySettings = inputOverlaySettingsViewModel.overlaySettings + ScreenContent( + appBarText = stringResource( + R.string.input_overlay_settings, + ), + navigateBack = navigateBack + ) + { + Toggle( + label = stringResource(R.string.input_overlay), + description = stringResource(R.string.enable_input_overlay), + initialCheckedState = { overlaySettings.isOverlayEnabled }, + onCheckedChanged = { overlaySettings.isOverlayEnabled = it } + ) + Toggle( + label = stringResource(R.string.vibrate), + description = stringResource(R.string.enable_vibrate_on_touch), + initialCheckedState = { overlaySettings.isVibrateOnTouchEnabled }, + onCheckedChanged = { overlaySettings.isVibrateOnTouchEnabled = it } + ) + Slider( + label = stringResource(R.string.alpha_slider), + initialValue = { overlaySettings.alpha }, + valueFrom = 0, + valueTo = 255, + onValueChange = { overlaySettings.alpha = it }, + labelFormatter = { it.toString() }, + ) + SingleSelection( + label = stringResource(R.string.overlay_controller), + initialChoice = { overlaySettings.controllerIndex }, + choices = ControllerIndexChoices, + choiceToString = { stringResource(R.string.controller_numbered, it + 1) }, + onChoiceChanged = { overlaySettings.controllerIndex = it } + ) + } +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/inputoverlay/InputOverlaySettingsViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/inputoverlay/InputOverlaySettingsViewModel.kt new file mode 100644 index 00000000..fecd1f47 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/inputoverlay/InputOverlaySettingsViewModel.kt @@ -0,0 +1,29 @@ +package info.cemu.cemu.settings.inputoverlay + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import info.cemu.cemu.CemuApplication +import info.cemu.cemu.inputoverlay.InputOverlaySettingsProvider + +class InputOverlaySettingsViewModel( + private val inputOverlaySettingsProvider: InputOverlaySettingsProvider +) : ViewModel() { + val overlaySettings = inputOverlaySettingsProvider.overlaySettings + + override fun onCleared() { + super.onCleared() + inputOverlaySettingsProvider.overlaySettings = overlaySettings + } + + companion object { + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + InputOverlaySettingsViewModel( + InputOverlaySettingsProvider(CemuApplication.Application) + ) + } + } + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/overlay/OverlaySettingsFragment.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/overlay/OverlaySettingsFragment.kt deleted file mode 100644 index 9c7d2b48..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/overlay/OverlaySettingsFragment.kt +++ /dev/null @@ -1,166 +0,0 @@ -package info.cemu.cemu.settings.overlay - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import info.cemu.cemu.R -import info.cemu.cemu.databinding.LayoutGenericRecyclerViewBinding -import info.cemu.cemu.guibasecomponents.GenericRecyclerViewAdapter -import info.cemu.cemu.guibasecomponents.HeaderRecyclerViewItem -import info.cemu.cemu.guibasecomponents.SingleSelectionRecyclerViewItem -import info.cemu.cemu.guibasecomponents.SliderRecyclerViewItem -import info.cemu.cemu.guibasecomponents.ToggleRecyclerViewItem -import info.cemu.cemu.nativeinterface.NativeSettings - -import java.util.List - -class OverlaySettingsFragment : Fragment() { - private fun overlayScreenPositionToString(overlayScreenPosition: Int): String { - val resourceId = when (overlayScreenPosition) { - NativeSettings.OVERLAY_SCREEN_POSITION_DISABLED -> R.string.overlay_position_disabled - NativeSettings.OVERLAY_SCREEN_POSITION_TOP_LEFT -> R.string.overlay_position_top_left - NativeSettings.OVERLAY_SCREEN_POSITION_TOP_CENTER -> R.string.overlay_position_top_center - NativeSettings.OVERLAY_SCREEN_POSITION_TOP_RIGHT -> R.string.overlay_position_top_right - NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_LEFT -> R.string.overlay_position_bottom_left - NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_CENTER -> R.string.overlay_position_bottom_center - NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_RIGHT -> R.string.overlay_position_bottom_right - else -> throw IllegalArgumentException("Invalid overlay position: $overlayScreenPosition") - } - return getString(resourceId) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val binding = LayoutGenericRecyclerViewBinding.inflate(inflater, container, false) - - val genericRecyclerViewAdapter = GenericRecyclerViewAdapter() - - val overlayPositionChoices = List.of( - NativeSettings.OVERLAY_SCREEN_POSITION_DISABLED, - NativeSettings.OVERLAY_SCREEN_POSITION_TOP_LEFT, - NativeSettings.OVERLAY_SCREEN_POSITION_TOP_CENTER, - NativeSettings.OVERLAY_SCREEN_POSITION_TOP_RIGHT, - NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_LEFT, - NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_CENTER, - NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_RIGHT - ) - - val overlayPositionSelection = SingleSelectionRecyclerViewItem( - getString(R.string.overlay_position), - NativeSettings.overlayPosition, - overlayPositionChoices, - { overlayScreenPosition: Int -> this.overlayScreenPositionToString(overlayScreenPosition) }, - NativeSettings::overlayPosition::set - ) - genericRecyclerViewAdapter.addRecyclerViewItem(overlayPositionSelection) - - val overlayTextScale = SliderRecyclerViewItem(getString(R.string.overlay_text_scale), - NativeSettings.OVERLAY_TEXT_SCALE_MIN.toFloat(), - NativeSettings.OVERLAY_TEXT_SCALE_MAX.toFloat(), - NativeSettings.overlayTextScalePercentage.toFloat(), - 25.0f, - { value: Float -> - NativeSettings.overlayTextScalePercentage = - value.toInt() - }, - { value: Float -> value.toInt().toString() + "%" }) - genericRecyclerViewAdapter.addRecyclerViewItem(overlayTextScale) - - val overlayFps = ToggleRecyclerViewItem( - getString(R.string.fps), - getString(R.string.fps_overlay_description), - NativeSettings.isOverlayFPSEnabled, - NativeSettings::isOverlayFPSEnabled::set - ) - genericRecyclerViewAdapter.addRecyclerViewItem(overlayFps) - - val drawCallsToggle = ToggleRecyclerViewItem( - getString(R.string.draw_calls_per_frame), - getString(R.string.draw_calls_per_frame_overlay_description), - NativeSettings.isOverlayDrawCallsPerFrameEnabled, - NativeSettings::isOverlayDrawCallsPerFrameEnabled::set - ) - genericRecyclerViewAdapter.addRecyclerViewItem(drawCallsToggle) - - val cpuUsageToggle = ToggleRecyclerViewItem( - getString(R.string.cpu_usage), - getString(R.string.cpu_usage_overlay_description), - NativeSettings.isOverlayCPUUsageEnabled, - NativeSettings::isOverlayCPUUsageEnabled::set - ) - genericRecyclerViewAdapter.addRecyclerViewItem(cpuUsageToggle) - - val ramUsageToggle = ToggleRecyclerViewItem( - getString(R.string.ram_usage), - getString(R.string.ram_usage_overlay_description), - NativeSettings.isOverlayRAMUsageEnabled, - NativeSettings::isOverlayRAMUsageEnabled::set - ) - genericRecyclerViewAdapter.addRecyclerViewItem(ramUsageToggle) - - val debugToggle = ToggleRecyclerViewItem( - getString(R.string.debug), - getString(R.string.debug_overlay_description), - NativeSettings.isOverlayDebugEnabled, - NativeSettings::isOverlayDebugEnabled::set - ) - genericRecyclerViewAdapter.addRecyclerViewItem(debugToggle) - - genericRecyclerViewAdapter.addRecyclerViewItem(HeaderRecyclerViewItem(R.string.notifications)) - - val notificationsPositionSelection = SingleSelectionRecyclerViewItem( - getString(R.string.overlay_position), - NativeSettings.notificationsPosition, - overlayPositionChoices, - { overlayScreenPosition: Int -> this.overlayScreenPositionToString(overlayScreenPosition) }, - NativeSettings::notificationsPosition::set - ) - genericRecyclerViewAdapter.addRecyclerViewItem(notificationsPositionSelection) - - val notificationTextScale = - SliderRecyclerViewItem(getString(R.string.notifications_text_scale), - NativeSettings.OVERLAY_TEXT_SCALE_MIN.toFloat(), - NativeSettings.OVERLAY_TEXT_SCALE_MAX.toFloat(), - NativeSettings.notificationsTextScalePercentage.toFloat(), - 25.0f, - { value: Float -> - NativeSettings.notificationsTextScalePercentage = - value.toInt() - }, - { value: Float -> value.toInt().toString() + "%" }) - genericRecyclerViewAdapter.addRecyclerViewItem(notificationTextScale) - - val controllerProfiles = ToggleRecyclerViewItem( - getString(R.string.controller_profiles), - getString(R.string.controller_profiles_notification_description), - NativeSettings.isNotificationControllerProfilesEnabled, - NativeSettings::isNotificationControllerProfilesEnabled::set - ) - genericRecyclerViewAdapter.addRecyclerViewItem(controllerProfiles) - - val shaderCompiler = ToggleRecyclerViewItem( - getString(R.string.shader_compiler), - getString(R.string.shader_compiler_notification_description), - NativeSettings.isNotificationShaderCompilerEnabled, - NativeSettings::isNotificationShaderCompilerEnabled::set - ) - genericRecyclerViewAdapter.addRecyclerViewItem(shaderCompiler) - - val friendList = ToggleRecyclerViewItem( - getString(R.string.friend_list), - getString(R.string.friend_list_notification_description), - NativeSettings.isNotificationFriendListEnabled, - NativeSettings::isNotificationFriendListEnabled::set - ) - genericRecyclerViewAdapter.addRecyclerViewItem(friendList) - - binding.recyclerView.adapter = genericRecyclerViewAdapter - - return binding.root - } -} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/overlay/OverlaySettingsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/overlay/OverlaySettingsScreen.kt new file mode 100644 index 00000000..6bb7d36b --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/overlay/OverlaySettingsScreen.kt @@ -0,0 +1,116 @@ +package info.cemu.cemu.settings.overlay + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import info.cemu.cemu.R +import info.cemu.cemu.guicore.Header +import info.cemu.cemu.guicore.ScreenContent +import info.cemu.cemu.guicore.SingleSelection +import info.cemu.cemu.guicore.Slider +import info.cemu.cemu.guicore.Toggle +import info.cemu.cemu.guicore.enumtostringmapper.native.overlayScreenPositionToStringId +import info.cemu.cemu.nativeinterface.NativeSettings + + +private val OverlayPositionChoices = listOf( + NativeSettings.OVERLAY_SCREEN_POSITION_DISABLED, + NativeSettings.OVERLAY_SCREEN_POSITION_TOP_LEFT, + NativeSettings.OVERLAY_SCREEN_POSITION_TOP_CENTER, + NativeSettings.OVERLAY_SCREEN_POSITION_TOP_RIGHT, + NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_LEFT, + NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_CENTER, + NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_RIGHT +) + +private const val OVERLAY_TEXT_SCALE_STEPS = 9 + +@Composable +fun OverlaySettingsScreen(navigateBack: () -> Unit) { + ScreenContent( + appBarText = stringResource(R.string.overlay_settings), + navigateBack = navigateBack, + ) { + Header(stringResource(R.string.overlay)) + SingleSelection( + label = stringResource(R.string.overlay_position), + initialChoice = NativeSettings::getOverlayPosition, + choices = OverlayPositionChoices, + choiceToString = { stringResource(overlayScreenPositionToStringId(it)) }, + onChoiceChanged = NativeSettings::setOverlayPosition, + ) + Slider( + label = stringResource(R.string.overlay_text_scale), + initialValue = NativeSettings::getOverlayTextScalePercentage, + steps = OVERLAY_TEXT_SCALE_STEPS, + valueFrom = NativeSettings.OVERLAY_TEXT_SCALE_MIN, + valueTo = NativeSettings.OVERLAY_TEXT_SCALE_MAX, + onValueChange = NativeSettings::setOverlayTextScalePercentage, + labelFormatter = { "${it}%" } + ) + Toggle( + label = stringResource(R.string.fps), + description = stringResource(R.string.fps_overlay_description), + initialCheckedState = NativeSettings::isOverlayFPSEnabled, + onCheckedChanged = NativeSettings::setOverlayFPSEnabled, + ) + Toggle( + label = stringResource(R.string.draw_calls_per_frame), + description = stringResource(R.string.draw_calls_per_frame_overlay_description), + initialCheckedState = NativeSettings::isOverlayDrawCallsPerFrameEnabled, + onCheckedChanged = NativeSettings::setOverlayDrawCallsPerFrameEnabled, + ) + Toggle( + label = stringResource(R.string.cpu_usage), + description = stringResource(R.string.cpu_usage_overlay_description), + initialCheckedState = NativeSettings::isOverlayCPUUsageEnabled, + onCheckedChanged = NativeSettings::setOverlayCPUUsageEnabled, + ) + Toggle( + label = stringResource(R.string.ram_usage), + description = stringResource(R.string.ram_usage_overlay_description), + initialCheckedState = NativeSettings::isOverlayRAMUsageEnabled, + onCheckedChanged = NativeSettings::setOverlayRAMUsageEnabled, + ) + Toggle( + label = stringResource(R.string.debug), + description = stringResource(R.string.debug_overlay_description), + initialCheckedState = NativeSettings::isOverlayDebugEnabled, + onCheckedChanged = NativeSettings::setOverlayDebugEnabled, + ) + Header(stringResource(R.string.notifications)) + SingleSelection( + label = stringResource(R.string.overlay_position), + initialChoice = NativeSettings::getNotificationsPosition, + choices = OverlayPositionChoices, + choiceToString = { stringResource(overlayScreenPositionToStringId(it)) }, + onChoiceChanged = NativeSettings::setNotificationsPosition, + ) + Slider( + label = stringResource(R.string.notifications_text_scale), + initialValue = NativeSettings::getNotificationsTextScalePercentage, + steps = OVERLAY_TEXT_SCALE_STEPS, + valueFrom = NativeSettings.OVERLAY_TEXT_SCALE_MIN, + valueTo = NativeSettings.OVERLAY_TEXT_SCALE_MAX, + onValueChange = NativeSettings::setNotificationsTextScalePercentage, + labelFormatter = { "$it%" } + ) + Toggle( + label = stringResource(R.string.controller_profiles), + description = stringResource(R.string.controller_profiles_notification_description), + initialCheckedState = NativeSettings::isNotificationControllerProfilesEnabled, + onCheckedChanged = NativeSettings::setNotificationControllerProfilesEnabled, + ) + Toggle( + label = stringResource(R.string.shader_compiler), + description = stringResource(R.string.shader_compiler_notification_description), + initialCheckedState = NativeSettings::isNotificationShaderCompilerEnabled, + onCheckedChanged = NativeSettings::setNotificationShaderCompilerEnabled, + ) + Toggle( + label = stringResource(R.string.friend_list), + description = stringResource(R.string.friend_list_notification_description), + initialCheckedState = NativeSettings::isNotificationFriendListEnabled, + onCheckedChanged = NativeSettings::setNotificationFriendListEnabled, + ) + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/zip/Zip.kt b/src/android/app/src/main/java/info/cemu/cemu/zip/Zip.kt index 4dc7e50d..3ad220f7 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/zip/Zip.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/zip/Zip.kt @@ -1,6 +1,5 @@ package info.cemu.cemu.zip -import java.io.File import java.io.FileOutputStream import java.io.IOException import java.io.InputStream @@ -8,31 +7,35 @@ import java.nio.file.Paths import java.util.zip.ZipEntry import java.util.zip.ZipInputStream +private const val DEFAULT_BUFFER_SIZE = 8192 + @Throws(IOException::class) -fun unzip(stream: InputStream?, targetDir: String?) { +fun unzip(stream: InputStream?, targetDir: String) { ZipInputStream(stream).use { zipInputStream -> var zipEntry: ZipEntry - val buffer = ByteArray(8192) + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) while ((zipInputStream.nextEntry.also { zipEntry = it }) != null) { - val filePath = Paths.get(targetDir, zipEntry.name).toString() - if (zipEntry.isDirectory) { - createDir(filePath) - } else { - FileOutputStream(filePath).use { fileOutputStream -> - var bytesRead: Int - while ((zipInputStream.read(buffer).also { bytesRead = it }) > 0) { - fileOutputStream.write(buffer, 0, bytesRead) - } - } - } + extractZipEntry(zipInputStream, zipEntry, buffer, targetDir) zipInputStream.closeEntry() } } } -private fun createDir(dir: String) { - val f = File(dir) - if (!f.isDirectory) { - f.mkdirs() +private fun extractZipEntry( + zipInputStream: ZipInputStream, + zipEntry: ZipEntry, + buffer: ByteArray, + targetDir: String, +) { + val file = Paths.get(targetDir, zipEntry.name).toFile() + if (zipEntry.isDirectory) { + file.apply { if (!isDirectory) mkdirs() } + return + } + FileOutputStream(file).use { fileOutputStream -> + var bytesRead: Int + while ((zipInputStream.read(buffer).also { bytesRead = it }) > 0) { + fileOutputStream.write(buffer, 0, bytesRead) + } } } diff --git a/src/android/app/src/main/res/drawable/ic_box.xml b/src/android/app/src/main/res/drawable/ic_box.xml new file mode 100644 index 00000000..e9335e69 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_box.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_build.xml b/src/android/app/src/main/res/drawable/ic_build.xml new file mode 100644 index 00000000..9c576818 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_build.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_controller.xml b/src/android/app/src/main/res/drawable/ic_controller.xml new file mode 100644 index 00000000..eb700e55 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_controller.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_filter.xml b/src/android/app/src/main/res/drawable/ic_filter.xml new file mode 100644 index 00000000..32654488 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_filter.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_question_mark.xml b/src/android/app/src/main/res/drawable/ic_question_mark.xml new file mode 100644 index 00000000..2d20020b --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_question_mark.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_save.xml b/src/android/app/src/main/res/drawable/ic_save.xml new file mode 100644 index 00000000..02cb0f71 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_save.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_upgrade.xml b/src/android/app/src/main/res/drawable/ic_upgrade.xml new file mode 100644 index 00000000..fcfd2b3e --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_upgrade.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/android/app/src/main/res/layout/activity_main.xml b/src/android/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 23c5ee37..00000000 --- a/src/android/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,12 +0,0 @@ - - diff --git a/src/android/app/src/main/res/layout/activity_settings.xml b/src/android/app/src/main/res/layout/activity_settings.xml deleted file mode 100644 index 21646275..00000000 --- a/src/android/app/src/main/res/layout/activity_settings.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/fragment_game_details.xml b/src/android/app/src/main/res/layout/fragment_game_details.xml deleted file mode 100644 index 8bcb57bc..00000000 --- a/src/android/app/src/main/res/layout/fragment_game_details.xml +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/fragment_game_profile_edit.xml b/src/android/app/src/main/res/layout/fragment_game_profile_edit.xml deleted file mode 100644 index 21a9b7b5..00000000 --- a/src/android/app/src/main/res/layout/fragment_game_profile_edit.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/fragment_games.xml b/src/android/app/src/main/res/layout/fragment_games.xml deleted file mode 100644 index 0a26763d..00000000 --- a/src/android/app/src/main/res/layout/fragment_games.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/src/android/app/src/main/res/layout/layout_button.xml b/src/android/app/src/main/res/layout/layout_button.xml deleted file mode 100644 index 1276e73b..00000000 --- a/src/android/app/src/main/res/layout/layout_button.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - diff --git a/src/android/app/src/main/res/layout/layout_checkbox.xml b/src/android/app/src/main/res/layout/layout_checkbox.xml deleted file mode 100644 index ca21b29c..00000000 --- a/src/android/app/src/main/res/layout/layout_checkbox.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - diff --git a/src/android/app/src/main/res/layout/layout_choice.xml b/src/android/app/src/main/res/layout/layout_choice.xml deleted file mode 100644 index 605b86e8..00000000 --- a/src/android/app/src/main/res/layout/layout_choice.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/layout_controller_input.xml b/src/android/app/src/main/res/layout/layout_controller_input.xml deleted file mode 100644 index 11c66a91..00000000 --- a/src/android/app/src/main/res/layout/layout_controller_input.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - diff --git a/src/android/app/src/main/res/layout/layout_game.xml b/src/android/app/src/main/res/layout/layout_game.xml deleted file mode 100644 index 28cd270f..00000000 --- a/src/android/app/src/main/res/layout/layout_game.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/android/app/src/main/res/layout/layout_game_path.xml b/src/android/app/src/main/res/layout/layout_game_path.xml deleted file mode 100644 index 0006b71d..00000000 --- a/src/android/app/src/main/res/layout/layout_game_path.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - diff --git a/src/android/app/src/main/res/layout/layout_generic_recycler_view.xml b/src/android/app/src/main/res/layout/layout_generic_recycler_view.xml deleted file mode 100644 index 178674ca..00000000 --- a/src/android/app/src/main/res/layout/layout_generic_recycler_view.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/layout_graphic_pack.xml b/src/android/app/src/main/res/layout/layout_graphic_pack.xml deleted file mode 100644 index 8a059059..00000000 --- a/src/android/app/src/main/res/layout/layout_graphic_pack.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/layout_graphic_pack_item.xml b/src/android/app/src/main/res/layout/layout_graphic_pack_item.xml deleted file mode 100644 index ea86c4f7..00000000 --- a/src/android/app/src/main/res/layout/layout_graphic_pack_item.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/layout_graphic_pack_search_item.xml b/src/android/app/src/main/res/layout/layout_graphic_pack_search_item.xml deleted file mode 100644 index e63f380a..00000000 --- a/src/android/app/src/main/res/layout/layout_graphic_pack_search_item.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/layout_graphic_packs_download.xml b/src/android/app/src/main/res/layout/layout_graphic_packs_download.xml deleted file mode 100644 index 2637fae5..00000000 --- a/src/android/app/src/main/res/layout/layout_graphic_packs_download.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/layout_header.xml b/src/android/app/src/main/res/layout/layout_header.xml deleted file mode 100644 index 81b16820..00000000 --- a/src/android/app/src/main/res/layout/layout_header.xml +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/src/android/app/src/main/res/layout/layout_simple_button.xml b/src/android/app/src/main/res/layout/layout_simple_button.xml deleted file mode 100644 index bc74c2ab..00000000 --- a/src/android/app/src/main/res/layout/layout_simple_button.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/src/android/app/src/main/res/layout/layout_single_selection.xml b/src/android/app/src/main/res/layout/layout_single_selection.xml deleted file mode 100644 index 5045823d..00000000 --- a/src/android/app/src/main/res/layout/layout_single_selection.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - diff --git a/src/android/app/src/main/res/layout/layout_single_selection_item.xml b/src/android/app/src/main/res/layout/layout_single_selection_item.xml deleted file mode 100644 index d1441a9c..00000000 --- a/src/android/app/src/main/res/layout/layout_single_selection_item.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/src/android/app/src/main/res/layout/layout_slider.xml b/src/android/app/src/main/res/layout/layout_slider.xml deleted file mode 100644 index cbf47ceb..00000000 --- a/src/android/app/src/main/res/layout/layout_slider.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - diff --git a/src/android/app/src/main/res/layout/layout_toggle.xml b/src/android/app/src/main/res/layout/layout_toggle.xml deleted file mode 100644 index a9d33601..00000000 --- a/src/android/app/src/main/res/layout/layout_toggle.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - diff --git a/src/android/app/src/main/res/menu/game.xml b/src/android/app/src/main/res/menu/game.xml deleted file mode 100644 index f72c2054..00000000 --- a/src/android/app/src/main/res/menu/game.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - diff --git a/src/android/app/src/main/res/menu/game_list.xml b/src/android/app/src/main/res/menu/game_list.xml deleted file mode 100644 index 956d0495..00000000 --- a/src/android/app/src/main/res/menu/game_list.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - \ No newline at end of file diff --git a/src/android/app/src/main/res/menu/game_paths.xml b/src/android/app/src/main/res/menu/game_paths.xml deleted file mode 100644 index eaffe49e..00000000 --- a/src/android/app/src/main/res/menu/game_paths.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/android/app/src/main/res/menu/graphic_packs.xml b/src/android/app/src/main/res/menu/graphic_packs.xml deleted file mode 100644 index 1c3ca347..00000000 --- a/src/android/app/src/main/res/menu/graphic_packs.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/src/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d1..00000000 Binary files a/src/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/src/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/src/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611da..00000000 Binary files a/src/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/src/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/src/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a6956..00000000 Binary files a/src/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f508..00000000 Binary files a/src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae37..00000000 Binary files a/src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/src/android/app/src/main/res/navigation/nav_main_graph.xml b/src/android/app/src/main/res/navigation/nav_main_graph.xml deleted file mode 100644 index dcf30a09..00000000 --- a/src/android/app/src/main/res/navigation/nav_main_graph.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/android/app/src/main/res/navigation/nav_settings_graph.xml b/src/android/app/src/main/res/navigation/nav_settings_graph.xml deleted file mode 100644 index 17001b3f..00000000 --- a/src/android/app/src/main/res/navigation/nav_settings_graph.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index b749c162..e6904bd9 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -78,9 +78,9 @@ VSync Accurate barriers Disabling the accurate barriers option will lead to flickering graphics but may improve performance. It is highly recommended to leave it turned on - TV + TV Enable audio output for the Wii U TV - Gamepad + Gamepad Enable audio output for the Wii U Gamepad Audio settings Mono @@ -89,12 +89,12 @@ TV channels Gamepad channels TV volume - Gamepad volume + Gamepad volume Search games No games were found Graphic packs Download latest community graphic packs - Download graphic packs + Download graphic packs Search Checking version Downloading graphic packs @@ -110,7 +110,7 @@ Graphic pack description This graphic pack has no description Graphic pack icon - Search graphic packs + Search graphic packs Edit input size Edit input positions Exited input edit mode @@ -167,7 +167,7 @@ Add game path Remove game path Game paths - Game path already added + Games path already added Quit Error Unable to launch game\nPath: %1$s @@ -246,4 +246,25 @@ Graphic pack is enabled Toggle screen Makes the \"show screen\" button toggle between the TV and gamepad screens + Load shared libraries + Load libraries from the cafeLibs directory + Shader multiplication accuracy + Controls the accuracy of floating point multiplication in shaders + Exit search + Clear search + Navigate back + Path + No icon found for game available + Installed only + GraphicPacksActivity2 + About Cemu + Version: %s + Cemu + Cemu is a Wii U emulator.\n\nWii and Wii U are trademarks of Nintendo.\nCemu is not affiliated with Nintendo. + Original authors: %s + Exzap, Petergov + Used libraries: + Website: + Modified ih264 from Android project + More options \ No newline at end of file diff --git a/src/android/app/src/main/res/values/themes.xml b/src/android/app/src/main/res/values/themes.xml index 13b21d94..203b9f57 100644 --- a/src/android/app/src/main/res/values/themes.xml +++ b/src/android/app/src/main/res/values/themes.xml @@ -8,9 +8,5 @@ @android:color/transparent @android:color/transparent ?attr/isLightTheme - @anim/nav_default_enter_anim - @anim/nav_default_exit_anim - @anim/nav_default_pop_enter_anim - @anim/nav_default_pop_exit_anim \ No newline at end of file diff --git a/src/android/build.gradle.kts b/src/android/build.gradle.kts index 67f437e9..d3527356 100644 --- a/src/android/build.gradle.kts +++ b/src/android/build.gradle.kts @@ -3,4 +3,6 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.kotlin.serialization) apply false } diff --git a/src/android/gradle/libs.versions.toml b/src/android/gradle/libs.versions.toml index 07d535cd..ecc27e68 100644 --- a/src/android/gradle/libs.versions.toml +++ b/src/android/gradle/libs.versions.toml @@ -1,22 +1,24 @@ [versions] -agp = "8.7.0" +agp = "8.7.3" appcompat = "1.7.0" -constraintlayout = "2.1.4" +constraintlayout = "2.2.0" datastoreRxjava3 = "1.1.1" -kotlin = "2.0.0" +kotlin = "2.0.21" coreKtx = "1.15.0" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" legacySupportV4 = "1.0.0" -lifecycleLivedataKtx = "2.8.6" +lifecycleLivedataKtx = "2.8.7" lifecycleRuntimeKtx = "2.8.7" activityCompose = "1.9.3" -composeBom = "2024.04.01" +composeBom = "2024.11.00" material = "1.12.0" -navigationFragment = "2.8.3" +navigationFragment = "2.8.4" okhttp = "4.12.0" +okhhtp-coroutines = "5.0.0-alpha.11" swiperefreshlayout = "1.1.0" +serializationVersion = "1.7.3" [libraries] androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } @@ -30,6 +32,7 @@ androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-lived androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleLivedataKtx" } androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "navigationFragment" } androidx-navigation-ui = { module = "androidx.navigation:navigation-ui", version.ref = "navigationFragment" } +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationFragment" } androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } @@ -43,13 +46,17 @@ androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } -androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } material = { module = "com.google.android.material:material", version.ref = "material" } + okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +okhttp-coroutines = { module = "com.squareup.okhttp3:okhttp-coroutines", version.ref = "okhhtp-coroutines" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serializationVersion" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/src/audio/IAudioAPI.cpp b/src/audio/IAudioAPI.cpp index d078900b..0e87ec76 100644 --- a/src/audio/IAudioAPI.cpp +++ b/src/audio/IAudioAPI.cpp @@ -75,7 +75,7 @@ void IAudioAPI::InitWFX(sint32 samplerate, sint32 channels, sint32 bits_per_samp void IAudioAPI::InitializeStatic() { - s_audioDelay = GetConfig().audio_delay; + SetAudioDelay(GetConfig().audio_delay); #if BOOST_OS_WINDOWS s_availableApis[DirectSound] = true; diff --git a/src/audio/IAudioAPI.h b/src/audio/IAudioAPI.h index 935e3024..9c94521b 100644 --- a/src/audio/IAudioAPI.h +++ b/src/audio/IAudioAPI.h @@ -59,7 +59,7 @@ public: static void PrintLogging(); static void InitializeStatic(); static bool IsAudioAPIAvailable(AudioAPI api); - + static void SetAudioDelay(uint32 audioDelay) { s_audioDelay = audioDelay; } static std::unique_ptr CreateDevice(AudioAPI api, const DeviceDescriptionPtr& device, sint32 samplerate, sint32 channels, sint32 samples_per_block, sint32 bits_per_sample); static std::vector GetDevices(AudioAPI api);