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);