mirror of
https://github.com/izzy2lost/WeeU.git
synced 2026-07-06 00:19:59 -07:00
Rewrite with compose
This commit is contained in:
@@ -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)
|
||||
|
||||
Vendored
+1
-21
@@ -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
|
||||
-dontobfuscate
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<uses-feature
|
||||
@@ -12,7 +11,7 @@
|
||||
android:version="0x401000" />
|
||||
|
||||
<application
|
||||
android:name="info.cemu.cemu.CemuApplication"
|
||||
android:name=".CemuApplication"
|
||||
android:allowBackup="true"
|
||||
android:appCategory="game"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
@@ -23,6 +22,27 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Cemu"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".about.AboutCemuActivity"
|
||||
android:exported="false"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".graphicpacks.GraphicPacksActivity"
|
||||
android:exported="false"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:exported="false"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".emulation.EmulationActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
@@ -33,6 +53,7 @@
|
||||
tools:ignore="DiscouragedApi">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:scheme="content" />
|
||||
@@ -51,19 +72,6 @@
|
||||
-->
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:exported="false"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name=".features.DocumentsProvider"
|
||||
|
||||
@@ -27,7 +27,7 @@ class AndroidFilesystemCallbacks : public FilesystemAndroid::FilesystemCallbacks
|
||||
AndroidFilesystemCallbacks()
|
||||
{
|
||||
JNIUtils::ScopedJNIENV env;
|
||||
m_fileUtilClass = JNIUtils::Scopedjclass("info/cemu/cemu/nativeinterface/FileCallbacksKt");
|
||||
m_fileUtilClass = JNIUtils::Scopedjclass("info/cemu/cemu/nativeinterface/NativeFilesKt");
|
||||
m_openContentUriMid = env->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");
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "GameTitleLoader.h"
|
||||
#include "JNIUtils.h"
|
||||
#include <android/bitmap.h>
|
||||
|
||||
// TODO: Refactor this:
|
||||
class AndroidGameTitleLoadedCallback : public GameTitleLoadedCallback
|
||||
{
|
||||
jmethodID m_onGameTitleLoadedMID;
|
||||
|
||||
@@ -6,6 +6,7 @@ add_library(CemuAndroid SHARED
|
||||
GameTitleLoader.cpp
|
||||
Image.cpp
|
||||
JNIUtils.cpp
|
||||
NativeActiveSettings.cpp
|
||||
NativeEmulation.cpp
|
||||
NativeGameTitles.cpp
|
||||
NativeGraphicPacks.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());
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
#include <Cafe/TitleList/SaveList.h>
|
||||
#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<uint64>()(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, "<init>", "(Ljava/lang/String;Ljava/lang/String;JJSI)V");
|
||||
m_callbackIdSaveList = CafeSaveList::RegisterCallback(
|
||||
[](CafeSaveListCallbackEvent* evt, void* ctx) {
|
||||
static_cast<SaveListCallback*>(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, "<init>", "(Ljava/lang/String;Ljava/lang/String;JJSIII)V");
|
||||
m_callbackIdTitleList = CafeTitleList::RegisterCallback(
|
||||
[](CafeTitleListCallbackEvent* evt, void* ctx) {
|
||||
static_cast<TitleListCallbacks*>(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<TitleListCallbacks> 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<TitleListCallbacks>(title_list_callbacks);
|
||||
}
|
||||
|
||||
std::unique_ptr<SaveListCallback> 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<SaveListCallback>(save_list_callback);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<MainRoutes.GamesRoute> {
|
||||
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<MainRoutes.GameDetailsRoute> {
|
||||
GameDetailsScreen(
|
||||
selectedGameViewModel = selectedGameViewModel,
|
||||
navigateBack = ::navigateBack,
|
||||
)
|
||||
}
|
||||
composable<MainRoutes.GameProfileEditRoute> {
|
||||
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 <T : ComponentActivity> Context.goToActivity(activityClass: Class<T>) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, String>) {
|
||||
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<Pair<String, String>> = 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"
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
+4
-5
@@ -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) {
|
||||
|
||||
@@ -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<String>?): 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<String>?): 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<String>?,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <T> TitleDetailsEntry(entryName: String, entryData: T?) {
|
||||
Column {
|
||||
Text(
|
||||
text = entryName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 24.sp,
|
||||
)
|
||||
Text(
|
||||
text = entryData?.toString() ?: "",
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<Set<Game>>(emptySet())
|
||||
val games: StateFlow<List<Game>> = 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<List<Game>> = MutableLiveData(emptyList())
|
||||
|
||||
init {
|
||||
NativeGameTitles.setGameTitleLoadedCallback(NativeGameTitles.GameTitleLoadedCallback { game: Game? ->
|
||||
if (game == null || !isGameValid(game)) {
|
||||
return@GameTitleLoadedCallback
|
||||
}
|
||||
_games.value += game
|
||||
})
|
||||
refreshGames()
|
||||
}
|
||||
|
||||
private val _gameToRemoveShaders = MutableStateFlow<Game?>(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()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user