mirror of
https://github.com/geode-sdk/android-launcher.git
synced 2026-10-22 16:42:38 +00:00
Compare commits
111
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
915e232011 | ||
|
|
cf4cd8335e | ||
|
|
2797e52683 | ||
|
|
69b00fecf5 | ||
|
|
f0eddd1652 | ||
|
|
d7190fa928 | ||
|
|
8e8a56da2e | ||
|
|
b418e154da | ||
|
|
cd306a5b5e | ||
|
|
fb9a6d1912 | ||
|
|
e276e302ab | ||
|
|
d7c779fce3 | ||
|
|
1e0dcc91ec | ||
|
|
266055e6d5 | ||
|
|
ab8e3f83d2 | ||
|
|
a75d68e5e0 | ||
|
|
13191dc66f | ||
|
|
b7f378b947 | ||
|
|
25367d5650 | ||
|
|
aa51c201e5 | ||
|
|
0b92f4a318 | ||
|
|
7d75825d01 | ||
|
|
c77271f45c | ||
|
|
09bb28d66f | ||
|
|
157952b6f7 | ||
|
|
352ff63ab0 | ||
|
|
91fc804dee | ||
|
|
87808b56b3 | ||
|
|
2c2884c3b8 | ||
|
|
74e91e7dc1 | ||
|
|
e3ffbf311e | ||
|
|
5ad77bca57 | ||
|
|
3be0127e04 | ||
|
|
458c837aba | ||
|
|
67ca7707d8 | ||
|
|
776a4fa468 | ||
|
|
9528eef313 | ||
|
|
9827fe36c1 | ||
|
|
ce44270b7a | ||
|
|
34e324c54d | ||
|
|
986e33ce03 | ||
|
|
91c06bde8c | ||
|
|
a9d40a7594 | ||
|
|
bb6146be37 | ||
|
|
071cf40d42 | ||
|
|
19f47f138a | ||
|
|
c070b5cfe0 | ||
|
|
4782431bae | ||
|
|
b63073c135 | ||
|
|
c029fed37d | ||
|
|
dacabda792 | ||
|
|
824d4395c7 | ||
|
|
bd502b56c2 | ||
|
|
6991598fcb | ||
|
|
2cc8e7215a | ||
|
|
5e81d1c70e | ||
|
|
b03ff6b735 | ||
|
|
220c1b0dd6 | ||
|
|
0a4fb091f4 | ||
|
|
110f74400c | ||
|
|
eed8020229 | ||
|
|
ceb3d5b461 | ||
|
|
53cd339213 | ||
|
|
327e9b4d39 | ||
|
|
10a0670a47 | ||
|
|
500d3ba015 | ||
|
|
1236180425 | ||
|
|
129ee40c0d | ||
|
|
a687f8464e | ||
|
|
b409a200bb | ||
|
|
acb07db6f2 | ||
|
|
0ae65971fc | ||
|
|
c4bd4333ca | ||
|
|
f9e30e549a | ||
|
|
1583f259b9 | ||
|
|
c090754129 | ||
|
|
31227860f4 | ||
|
|
edc2428f48 | ||
|
|
6f3473b4ca | ||
|
|
f0af12f466 | ||
|
|
5bea87f98c | ||
|
|
8617d900ee | ||
|
|
1d91357cad | ||
|
|
1d495cb26e | ||
|
|
31b5a8f16e | ||
|
|
b1c8690872 | ||
|
|
c01470ac5a | ||
|
|
98d3b76e4b | ||
|
|
4a5e4ce85c | ||
|
|
d3a3ab57a1 | ||
|
|
86a79f8cda | ||
|
|
5fdf3127ae | ||
|
|
e9635160d1 | ||
|
|
4e6f99c745 | ||
|
|
1fd72ba8bc | ||
|
|
03e12bf701 | ||
|
|
4ae10dba7b | ||
|
|
cad559f1d9 | ||
|
|
b60c564173 | ||
|
|
7c21431efe | ||
|
|
d345f4ccf8 | ||
|
|
6b70d8c6d0 | ||
|
|
5f8e2b0ac3 | ||
|
|
fa7a8d96ea | ||
|
|
291237d4f5 | ||
|
|
d4e85e3055 | ||
|
|
78992316e5 | ||
|
|
70333e3b5f | ||
|
|
492e1ac11c | ||
|
|
3daf5c48f0 | ||
|
|
e9e250191c |
No files matched your search
@@ -1,9 +1,9 @@
|
||||
name: Update Website
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- released
|
||||
# release:
|
||||
# types:
|
||||
# - released
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
+34
-20
@@ -1,5 +1,13 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
val composeBOM: String by rootProject.extra
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
extraWarnings.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
@@ -8,14 +16,14 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 34
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.geode.launcher"
|
||||
minSdk = 23
|
||||
targetSdk = 35
|
||||
versionCode = 17
|
||||
versionName = "1.4.0"
|
||||
versionCode = 22
|
||||
versionName = "1.6.1"
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
@@ -24,7 +32,7 @@ android {
|
||||
@Suppress("UnstableApiUsage")
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments("-DUSE_TULIPHOOK:BOOL=OFF", "-DANDROID_STL=c++_shared")
|
||||
arguments("-DUSE_TULIPHOOK:BOOL=OFF", "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES:BOOL=OFF")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +70,10 @@ android {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_1_8
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
@@ -76,30 +86,34 @@ android {
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
version = "3.25.0+"
|
||||
path = file("src/main/cpp/CMakeLists.txt")
|
||||
}
|
||||
}
|
||||
namespace = "com.geode.launcher"
|
||||
ndkVersion = "26.3.11579264"
|
||||
ndkVersion = "28.1.13356709"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation (platform("androidx.compose:compose-bom:$composeBOM"))
|
||||
implementation ("androidx.core:core-ktx:1.13.1")
|
||||
implementation ("androidx.core:core-ktx:1.17.0")
|
||||
implementation ("androidx.compose.ui:ui")
|
||||
implementation ("androidx.compose.material3:material3")
|
||||
implementation ("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.8.2")
|
||||
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2")
|
||||
implementation ("androidx.activity:activity-compose:1.9.0")
|
||||
implementation ("androidx.activity:activity-ktx:1.9.0")
|
||||
implementation ("androidx.appcompat:appcompat:1.7.0")
|
||||
implementation ("androidx.documentfile:documentfile:1.0.1")
|
||||
implementation ("com.squareup.okio:okio:3.9.0")
|
||||
implementation ("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
|
||||
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.7.0")
|
||||
implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0")
|
||||
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.9.3")
|
||||
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.3")
|
||||
implementation ("androidx.activity:activity-compose:1.11.0")
|
||||
implementation ("androidx.activity:activity-ktx:1.11.0")
|
||||
implementation ("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation ("androidx.documentfile:documentfile:1.1.0")
|
||||
implementation ("com.squareup.okio:okio:3.16.0")
|
||||
implementation ("com.squareup.okhttp3:okhttp:5.1.0")
|
||||
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.9.0")
|
||||
implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
|
||||
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.37.0")
|
||||
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.37.0")
|
||||
implementation ("androidx.browser:browser:1.9.0")
|
||||
debugImplementation ("androidx.compose.ui:ui-tooling")
|
||||
coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.0.4")
|
||||
coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.1.5")
|
||||
}
|
||||
@@ -21,6 +21,8 @@
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.LIGHTS" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.wifi"
|
||||
@@ -28,6 +30,9 @@
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00020000"
|
||||
android:required="true" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.gamepad"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -36,9 +41,11 @@
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:hasFragileUserData="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:theme="@style/Theme.GeodeLauncher"
|
||||
tools:targetApi="31">
|
||||
tools:targetApi="35">
|
||||
<activity
|
||||
android:name=".AltMainActivity"
|
||||
android:exported="false"
|
||||
@@ -53,14 +60,21 @@
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_application_logs"
|
||||
android:theme="@style/Theme.GeodeLauncher" />
|
||||
<activity
|
||||
android:name=".preferences.CrashLogsActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_crash_logs"
|
||||
android:theme="@style/Theme.GeodeLauncher" />
|
||||
<activity
|
||||
android:name=".preferences.ApplicationIconActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_application_icon"
|
||||
android:theme="@style/Theme.GeodeLauncher" />
|
||||
<activity
|
||||
android:name=".preferences.SettingsActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_settings"
|
||||
android:theme="@style/Theme.GeodeLauncher">
|
||||
<meta-data
|
||||
android:name="android.app.lib_name"
|
||||
android:value="" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".GeometryDashActivity"
|
||||
@@ -80,11 +94,49 @@
|
||||
android:theme="@style/Theme.GeodeLauncher">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityGeode"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher_geode"
|
||||
android:targetActivity=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityPride"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher_pride"
|
||||
android:targetActivity=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityTrans"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher_trans"
|
||||
android:targetActivity=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<provider
|
||||
android:name=".UserDirectoryProvider"
|
||||
android:authorities="com.geode.launcher.user"
|
||||
|
||||
@@ -13,7 +13,7 @@ option(USE_TULIPHOOK "enables choice of hooking library" OFF)
|
||||
if (USE_TULIPHOOK)
|
||||
FetchContent_Declare(TulipHook
|
||||
GIT_REPOSITORY https://github.com/geode-sdk/TulipHook.git
|
||||
GIT_TAG 0021301fcea265416fa8eb5372694931c1d410cb
|
||||
GIT_TAG v3.1.4
|
||||
)
|
||||
|
||||
set(TULIP_LINK_SOURCE ON CACHE INTERNAL "")
|
||||
@@ -24,8 +24,9 @@ if (USE_TULIPHOOK)
|
||||
target_link_libraries(launcherfix PRIVATE TulipHook TulipHookInclude)
|
||||
else()
|
||||
FetchContent_Declare(dobby
|
||||
# this is the last version that builds on android at all
|
||||
GIT_REPOSITORY https://github.com/jmpews/Dobby
|
||||
GIT_TAG 6a80a61ea062424d1595613058a174c64454fa57
|
||||
GIT_TAG 0932d69c320e786672361ab53825ba8f4245e9d3
|
||||
)
|
||||
|
||||
FetchContent_GetProperties(dobby)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <jni.h>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <android/log.h>
|
||||
#include <link.h>
|
||||
@@ -59,16 +60,6 @@ JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_enableCustomSymbolLis
|
||||
env->ReleaseStringUTFChars(symbol_path, symbol_path_str);
|
||||
}
|
||||
|
||||
#ifdef __arm__
|
||||
// 32bit code
|
||||
typedef Elf32_Dyn Elf_Dyn;
|
||||
typedef Elf32_Sym Elf_Sym;
|
||||
#else
|
||||
// otherwise we're probably 64bit
|
||||
typedef Elf64_Dyn Elf_Dyn;
|
||||
typedef Elf64_Sym Elf_Sym;
|
||||
#endif
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_setOriginalDataPath(
|
||||
JNIEnv *env,
|
||||
@@ -98,7 +89,9 @@ std::uint32_t elfhash(const char *_name) {
|
||||
return h;
|
||||
}
|
||||
|
||||
bool patch_symbol(std::uint32_t* hash_table, char* str_table, Elf_Sym* sym_table, const char* orig_name) {
|
||||
std::array<std::uint8_t, 3> SYMBOL_PATCH_VALUE{":3"};
|
||||
|
||||
bool patch_symbol(std::uint32_t* hash_table, char* str_table, ElfW(Sym)* sym_table, const char* orig_name) {
|
||||
auto hash = elfhash(orig_name);
|
||||
|
||||
auto nbucket = hash_table[0];
|
||||
@@ -114,16 +107,15 @@ bool patch_symbol(std::uint32_t* hash_table, char* str_table, Elf_Sym* sym_table
|
||||
// there's probably no point to checking this, honestly
|
||||
if (ELF_ST_BIND(sym->st_info) == STB_GLOBAL && ELF_ST_TYPE(sym->st_info) == STT_FUNC) {
|
||||
// we found it! now go rename the symbol
|
||||
std::array<std::uint8_t, 3> new_symbol{":3"};
|
||||
|
||||
#ifdef USE_TULIPHOOK
|
||||
auto res = tulip::hook::writeMemory(sym_name, new_symbol.data(), new_symbol.size());
|
||||
auto res = tulip::hook::writeMemory(sym_name, SYMBOL_PATCH_VALUE.data(), SYMBOL_PATCH_VALUE.size());
|
||||
if (!res) {
|
||||
__android_log_print(ANDROID_LOG_WARN, "GeodeLauncher-fix", "failed to patch symbol: %s", res.unwrapErr().c_str());
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
DobbyCodePatch(reinterpret_cast<void*>(sym_name), new_symbol.data(), new_symbol.size());
|
||||
DobbyCodePatch(reinterpret_cast<void*>(sym_name), SYMBOL_PATCH_VALUE.data(), SYMBOL_PATCH_VALUE.size());
|
||||
#endif
|
||||
|
||||
return true;
|
||||
@@ -131,7 +123,7 @@ bool patch_symbol(std::uint32_t* hash_table, char* str_table, Elf_Sym* sym_table
|
||||
}
|
||||
}
|
||||
|
||||
__android_log_print(ANDROID_LOG_WARN, "GeodeLauncher-fix", "could not find symbol %s to patch", orig_name);
|
||||
__android_log_print(ANDROID_LOG_WARN, "GeodeLauncher-fix", "could not find symbol %s to patch (hash: %u)", orig_name, hash);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -149,9 +141,12 @@ std::vector<std::string> get_symbols_listing() {
|
||||
"__cxa_guard_abort",
|
||||
"__cxa_guard_acquire",
|
||||
"__cxa_guard_release",
|
||||
"__cxa_free_exception",
|
||||
"_Unwind_RaiseException",
|
||||
"_Unwind_Resume"
|
||||
"__cxa_free_exception"
|
||||
|
||||
// android 10's libc doesn't have these symbols
|
||||
// and the ndk is forbidden from exporting them!! so keep it for old mods...
|
||||
// "_Unwind_RaiseException",
|
||||
// "_Unwind_Resume"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -174,7 +169,7 @@ std::vector<std::string> get_symbols_listing() {
|
||||
|
||||
int on_dl_iterate(dl_phdr_info* info, size_t size, void* data) {
|
||||
// this is probably going to be gd
|
||||
if (strstr(info->dlpi_name, "libcocos2dcpp.so") != nullptr) {
|
||||
if (info->dlpi_name != nullptr && strstr(info->dlpi_name, "libcocos2dcpp.so") != nullptr) {
|
||||
// step 1: get the dynamic table
|
||||
std::uintptr_t dyn_addr = 0u;
|
||||
|
||||
@@ -193,12 +188,13 @@ int on_dl_iterate(dl_phdr_info* info, size_t size, void* data) {
|
||||
}
|
||||
|
||||
// step 2: get the symbol table
|
||||
auto dyn_entry = reinterpret_cast<Elf_Dyn*>(dyn_addr);
|
||||
auto dyn_entry = reinterpret_cast<ElfW(Dyn)*>(dyn_addr);
|
||||
auto dyn_end_reached = false;
|
||||
|
||||
std::uintptr_t sym_table_addr = 0u;
|
||||
std::uintptr_t str_table_addr = 0u;
|
||||
std::uintptr_t hash_table_addr = 0u;
|
||||
std::uintptr_t soname_idx = 0u;
|
||||
|
||||
while (!dyn_end_reached) {
|
||||
auto tag = dyn_entry->d_tag;
|
||||
@@ -213,6 +209,9 @@ int on_dl_iterate(dl_phdr_info* info, size_t size, void* data) {
|
||||
case DT_HASH:
|
||||
hash_table_addr = info->dlpi_addr + dyn_entry->d_un.d_val;
|
||||
break;
|
||||
case DT_SONAME:
|
||||
soname_idx = dyn_entry->d_un.d_val;
|
||||
break;
|
||||
case DT_NULL:
|
||||
dyn_end_reached = true;
|
||||
break;
|
||||
@@ -222,18 +221,35 @@ int on_dl_iterate(dl_phdr_info* info, size_t size, void* data) {
|
||||
}
|
||||
|
||||
if (hash_table_addr == 0u || str_table_addr == 0u || sym_table_addr == 0u) {
|
||||
__android_log_print(ANDROID_LOG_WARN, "GeodeLauncher-fix", "failed to parse dynamic section (at least one table is null)");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "GeodeLauncher-fix", "failed to parse dynamic section (at least one table is null)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// patch symbol names
|
||||
auto hash_table = reinterpret_cast<std::uint32_t*>(hash_table_addr);
|
||||
auto str_table = reinterpret_cast<char*>(str_table_addr);
|
||||
auto sym_table = reinterpret_cast<Elf_Sym*>(sym_table_addr);
|
||||
auto sym_table = reinterpret_cast<ElfW(Sym)*>(sym_table_addr);
|
||||
|
||||
auto so_name = str_table + soname_idx;
|
||||
if (strcmp(so_name, "libcocos2dcpp.so") != 0) {
|
||||
__android_log_print(ANDROID_LOG_WARN, "GeodeLauncher-fix", "DT_SONAME gives us %s! that's not good", so_name);
|
||||
}
|
||||
|
||||
auto has_error = false;
|
||||
auto symbols_listing = get_symbols_listing();
|
||||
for (const auto& symbol : symbols_listing) {
|
||||
patch_symbol(hash_table, str_table, sym_table, symbol.c_str());
|
||||
if (!patch_symbol(hash_table, str_table, sym_table, symbol.c_str())) {
|
||||
has_error = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_error) {
|
||||
__android_log_print(
|
||||
ANDROID_LOG_INFO,
|
||||
"GeodeLauncher-fix",
|
||||
"symbol patch diagnostics\nlibrary path: %s\naddrs: base=%p sym=%p str=%p hash=%p",
|
||||
info->dlpi_name, reinterpret_cast<void*>(info->dlpi_addr), sym_table, str_table, hash_table
|
||||
);
|
||||
}
|
||||
|
||||
return 1;
|
||||
@@ -249,9 +265,9 @@ JNIEXPORT int JNICALL Java_com_geode_launcher_LauncherFix_performExceptionsRenam
|
||||
return dl_iterate_phdr(on_dl_iterate, nullptr);
|
||||
}
|
||||
|
||||
std::string redirect_path(const char* pathname) {
|
||||
auto data_path = DataPaths::get_instance().data_path;
|
||||
auto original_data_path = DataPaths::get_instance().original_data_path;
|
||||
std::optional<std::string> redirect_path(const char* pathname) {
|
||||
auto& data_path = DataPaths::get_instance().data_path;
|
||||
auto& original_data_path = DataPaths::get_instance().original_data_path;
|
||||
|
||||
if (!data_path.empty() && !original_data_path.empty()) {
|
||||
// call this a c string optimization
|
||||
@@ -262,22 +278,26 @@ std::string redirect_path(const char* pathname) {
|
||||
}
|
||||
}
|
||||
|
||||
return pathname;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
FILE* (*fopen_original)(const char *pathname, const char *mode);
|
||||
FILE* fopen_hook(const char* pathname, const char* mode) {
|
||||
auto path_str = redirect_path(pathname);
|
||||
auto redirect = path_str.has_value() ? path_str->c_str() : pathname;
|
||||
|
||||
return fopen_original(path_str.c_str(), mode);
|
||||
return fopen_original(redirect, mode);
|
||||
}
|
||||
|
||||
int (*rename_original)(const char* old_path, const char* new_path);
|
||||
int rename_hook(const char* old_path, const char* new_path) {
|
||||
auto old_path_str = redirect_path(old_path);
|
||||
auto new_path_str = redirect_path(new_path);
|
||||
auto old_redirect = old_path_str ? old_path_str->c_str() : old_path;
|
||||
|
||||
return rename_original(old_path_str.c_str(), new_path_str.c_str());
|
||||
auto new_path_str = redirect_path(new_path);
|
||||
auto new_redirect = new_path_str ? new_path_str->c_str() : new_path;
|
||||
|
||||
return rename_original(old_redirect, new_redirect);
|
||||
}
|
||||
|
||||
bool hook_function(void* addr, auto* hook, auto** orig) {
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.customRobTop
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.KeyguardManager
|
||||
import android.content.*
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
@@ -11,7 +10,6 @@ import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.customRobTop.JniToCpp.resumeSound
|
||||
import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.closeIMEKeyboard
|
||||
import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.openIMEKeyboard
|
||||
import java.lang.ref.WeakReference
|
||||
@@ -259,29 +257,4 @@ object BaseRobTopActivity {
|
||||
level = DeprecationLevel.HIDDEN
|
||||
)
|
||||
fun logEvent(event: String) {}
|
||||
|
||||
class ReceiverScreen : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
Intent.ACTION_SCREEN_ON -> {
|
||||
Log.d("TAG", "ACTION_SCREEN_ON")
|
||||
if (!(me.get()?.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager).isKeyguardLocked) {
|
||||
shouldResumeSound = true
|
||||
}
|
||||
if (!isPaused && shouldResumeSound) {
|
||||
resumeSound()
|
||||
}
|
||||
}
|
||||
Intent.ACTION_SCREEN_OFF -> {
|
||||
shouldResumeSound = false
|
||||
}
|
||||
Intent.ACTION_USER_PRESENT -> {
|
||||
shouldResumeSound = true
|
||||
if (!isPaused) {
|
||||
resumeSound()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -24,13 +24,12 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
@@ -55,11 +54,14 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.geode.launcher.main.*
|
||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||
import com.geode.launcher.ui.theme.LocalTheme
|
||||
import com.geode.launcher.ui.theme.Theme
|
||||
import com.geode.launcher.ui.theme.launcherTitleStyle
|
||||
import com.geode.launcher.utils.Constants
|
||||
import com.geode.launcher.utils.GamePackageUtils
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
@@ -94,6 +96,7 @@ class AltMainActivity : ComponentActivity() {
|
||||
val theme = Theme.fromInt(themeOption)
|
||||
|
||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||
|
||||
val launchViewModel = viewModel<LaunchViewModel>(factory = LaunchViewModel.Factory)
|
||||
|
||||
@@ -102,7 +105,7 @@ class AltMainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
@@ -123,7 +126,7 @@ class AltMainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
data class LaunchStatusInfo(
|
||||
val title: String,
|
||||
val title: String?,
|
||||
val details: String? = null,
|
||||
val progress: (() -> Float)? = null
|
||||
)
|
||||
@@ -156,8 +159,9 @@ fun mapCancelReasonToInfo(cancelReason: LaunchViewModel.LaunchCancelReason): Lau
|
||||
title = stringResource(R.string.launcher_cancelled_error),
|
||||
details = stringResource(R.string.launcher_cancelled_outdated)
|
||||
)
|
||||
LaunchViewModel.LaunchCancelReason.AUTOMATIC -> LaunchStatusInfo(
|
||||
title = stringResource(R.string.launcher_cancelled_navigated_away)
|
||||
LaunchViewModel.LaunchCancelReason.AUTOMATIC,
|
||||
LaunchViewModel.LaunchCancelReason.MANUAL-> LaunchStatusInfo(
|
||||
title = null
|
||||
)
|
||||
else -> LaunchStatusInfo(
|
||||
title = stringResource(R.string.launcher_cancelled_manual)
|
||||
@@ -185,16 +189,18 @@ fun mapLaunchStatusToInfo(state: LaunchViewModel.LaunchUIState, inSafeMode: Bool
|
||||
}
|
||||
|
||||
val outOf = remember(state.outOf) {
|
||||
Formatter.formatShortFileSize(context, state.outOf)
|
||||
state.outOf?.run {
|
||||
Formatter.formatShortFileSize(context, this)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchStatusInfo(
|
||||
title = stringResource(R.string.launcher_downloading_update),
|
||||
details = stringResource(R.string.launcher_downloading_update_details, downloaded, outOf),
|
||||
details = stringResource(R.string.launcher_downloading_update_details, downloaded, outOf ?: "…"),
|
||||
progress = {
|
||||
val progress = state.downloaded / state.outOf.toDouble()
|
||||
val progress = state.downloaded / state.outOf!!.toDouble()
|
||||
progress.toFloat()
|
||||
}
|
||||
}.takeIf { state.outOf != null }
|
||||
)
|
||||
}
|
||||
is LaunchViewModel.LaunchUIState.Cancelled -> {
|
||||
@@ -205,16 +211,22 @@ fun mapLaunchStatusToInfo(state: LaunchViewModel.LaunchUIState, inSafeMode: Bool
|
||||
|
||||
@Composable
|
||||
fun LaunchCancelledBody(statusInfo: LaunchStatusInfo, icon: @Composable () -> Unit, inProgress: Boolean, modifier: Modifier = Modifier) {
|
||||
if (statusInfo.title == null && statusInfo.details == null && !inProgress) {
|
||||
return
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier.width(300.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
|
||||
icon()
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(statusInfo.title)
|
||||
if (statusInfo.title != null) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
|
||||
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
|
||||
icon()
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(statusInfo.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +237,8 @@ fun LaunchCancelledBody(statusInfo: LaunchStatusInfo, icon: @Composable () -> Un
|
||||
if (statusInfo.details != null) {
|
||||
Text(
|
||||
statusInfo.details,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -239,7 +252,9 @@ fun LaunchProgressBody(statusInfo: LaunchStatusInfo, modifier: Modifier = Modifi
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = modifier.width(300.dp)
|
||||
) {
|
||||
Text(statusInfo.title, style = MaterialTheme.typography.bodyLarge)
|
||||
if (statusInfo.title != null) {
|
||||
Text(statusInfo.title, style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
|
||||
if (statusInfo.progress != null) {
|
||||
LinearProgressIndicator(
|
||||
@@ -259,16 +274,7 @@ fun LaunchProgressBody(statusInfo: LaunchStatusInfo, modifier: Modifier = Modifi
|
||||
@Composable
|
||||
fun RetryButtonContents(reason: LaunchViewModel.LaunchCancelReason) {
|
||||
when (reason) {
|
||||
LaunchViewModel.LaunchCancelReason.MANUAL -> {
|
||||
Icon(
|
||||
Icons.Filled.Refresh,
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_cancelled_restart))
|
||||
}
|
||||
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED,
|
||||
LaunchViewModel.LaunchCancelReason.AUTOMATIC -> {
|
||||
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_resume),
|
||||
contentDescription = null
|
||||
@@ -276,6 +282,15 @@ fun RetryButtonContents(reason: LaunchViewModel.LaunchCancelReason) {
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_cancelled_resume))
|
||||
}
|
||||
LaunchViewModel.LaunchCancelReason.MANUAL,
|
||||
LaunchViewModel.LaunchCancelReason.AUTOMATIC -> {
|
||||
Icon(
|
||||
Icons.Filled.PlayArrow,
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_launch))
|
||||
}
|
||||
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> {
|
||||
Icon(
|
||||
Icons.Filled.Refresh,
|
||||
@@ -305,8 +320,8 @@ fun LaunchProgressCard(
|
||||
var showSafeModeDialog by remember { mutableStateOf(false) }
|
||||
val status = mapLaunchStatusToInfo(uiState, safeModeEnabled)
|
||||
|
||||
Card(modifier = modifier.width(300.dp)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.padding(16.dp)) {
|
||||
Box(modifier = modifier) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
if (uiState is LaunchViewModel.LaunchUIState.Cancelled) {
|
||||
LaunchCancelledBody(
|
||||
statusInfo = status,
|
||||
@@ -314,13 +329,9 @@ fun LaunchProgressCard(
|
||||
inProgress = uiState.inProgress
|
||||
)
|
||||
|
||||
if (uiState.reason == LaunchViewModel.LaunchCancelReason.GAME_OUTDATED) {
|
||||
if (uiState.reason.isGameInstallIssue()) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// game should be downloaded if this message is showing
|
||||
val showDownload = remember {
|
||||
!GamePackageUtils.identifyGameLegitimacy(context.packageManager)
|
||||
}
|
||||
val showDownload = remember { GamePackageUtils.showDownloadBadge(context.packageManager) }
|
||||
|
||||
if (showDownload) {
|
||||
GooglePlayBadge(modifier = Modifier.align(Alignment.CenterHorizontally))
|
||||
@@ -329,8 +340,8 @@ fun LaunchProgressCard(
|
||||
|
||||
if (uiState.reason.allowsRetry()) {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.width(300.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
LongPressButton(onClick = { onResume(false) }, onLongPress = {
|
||||
showSafeModeDialog = true
|
||||
@@ -338,14 +349,14 @@ fun LaunchProgressCard(
|
||||
RetryButtonContents(uiState.reason)
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1.0f))
|
||||
|
||||
if (crashInfo != null) {
|
||||
IconButton(onClick = onMore) {
|
||||
FilledTonalButton(onClick = onMore) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_question_mark),
|
||||
contentDescription = stringResource(R.string.launcher_cancelled_help_alt)
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_cancelled_help_alt))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +364,7 @@ fun LaunchProgressCard(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LaunchProgressBody(statusInfo = status)
|
||||
LaunchProgressBody(statusInfo = status, modifier = Modifier.padding(top = 16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.width(300.dp)) {
|
||||
TextButton(onClick = onCancel) {
|
||||
@@ -400,15 +411,18 @@ fun GeodeLogo(modifier: Modifier = Modifier) {
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.geode_logo),
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.geode_monochrome),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(76.dp, 76.dp)
|
||||
modifier = Modifier
|
||||
.size(64.dp, 64.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.launcher_title),
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
modifier = Modifier.padding(12.dp)
|
||||
style = launcherTitleStyle,
|
||||
fontSize = 64.sp,
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -436,10 +450,9 @@ fun AltMainScreen(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.height(350.dp)
|
||||
.padding(8.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
@@ -483,27 +496,18 @@ fun AltMainScreen(
|
||||
safeModeEnabled = launchInSafeMode
|
||||
)
|
||||
|
||||
val nextLauncherUpdate by launchViewModel.nextLauncherUpdate.collectAsState()
|
||||
|
||||
// only show launcher update in a case where a user won't see it ingame
|
||||
if (nextLauncherUpdate != null && launchUIState is LaunchViewModel.LaunchUIState.Cancelled) {
|
||||
StatusIndicator(
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Default.Info,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
installLauncherUpdate(context)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
stringResource(R.string.launcher_update_available),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
)
|
||||
if (launchUIState is LaunchViewModel.LaunchUIState.Cancelled) {
|
||||
val gameVersion = remember { GamePackageUtils.getGameVersionCodeOrNull(context.packageManager) }
|
||||
|
||||
if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
|
||||
UnsupportedVersionWarning()
|
||||
}
|
||||
|
||||
val nextLauncherUpdate by launchViewModel.nextLauncherUpdate.collectAsState()
|
||||
if (nextLauncherUpdate != null) {
|
||||
LauncherUpdateIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
package com.geode.launcher
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.opengl.GLSurfaceView
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
@@ -23,6 +25,7 @@ import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.customRobTop.BaseRobTopActivity
|
||||
import com.customRobTop.JniToCpp
|
||||
import com.geode.launcher.main.LaunchNotification
|
||||
import com.geode.launcher.main.determineDisplayedCards
|
||||
import com.geode.launcher.utils.Constants
|
||||
import com.geode.launcher.utils.ConstrainedFrameLayout
|
||||
import com.geode.launcher.utils.DownloadUtils
|
||||
@@ -30,7 +33,6 @@ import com.geode.launcher.utils.GamePackageUtils
|
||||
import com.geode.launcher.utils.GeodeUtils
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import org.cocos2dx.lib.Cocos2dxEditText
|
||||
import org.cocos2dx.lib.Cocos2dxGLSurfaceView
|
||||
import org.cocos2dx.lib.Cocos2dxHelper
|
||||
import org.cocos2dx.lib.Cocos2dxRenderer
|
||||
@@ -39,6 +41,9 @@ import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import javax.microedition.khronos.egl.EGL10
|
||||
import javax.microedition.khronos.egl.EGLConfig
|
||||
import javax.microedition.khronos.egl.EGLDisplay
|
||||
|
||||
enum class DisplayMode {
|
||||
DEFAULT, LIMITED, FULLSCREEN;
|
||||
@@ -52,19 +57,29 @@ enum class DisplayMode {
|
||||
}
|
||||
}
|
||||
|
||||
fun ratioForPreference(value: String) = when (value) {
|
||||
"4_3" -> 4/3.0f
|
||||
"1_1" -> 1.0f
|
||||
"16_10" -> 16/10.0f
|
||||
else -> 1.77f
|
||||
}
|
||||
|
||||
class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperListener, GeodeUtils.CapabilityListener {
|
||||
private var mGLSurfaceView: Cocos2dxGLSurfaceView? = null
|
||||
private val sTag = GeometryDashActivity::class.simpleName
|
||||
private var mIsRunning = false
|
||||
private var mIsOnPause = false
|
||||
private var mHasWindowFocus = false
|
||||
private var mReceiver: BroadcastReceiver? = null
|
||||
|
||||
private var displayMode = DisplayMode.DEFAULT
|
||||
private var forceRefreshRate = false
|
||||
private var mForceRefreshRate = false
|
||||
private var mLimitedRefreshRate: Int? = null
|
||||
private var mAspectRatio = 0.0f
|
||||
private var mScreenZoom = 1.0f
|
||||
private var mScreenZoomFit = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setupUIState()
|
||||
FMOD.init(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -88,6 +103,8 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
val errorMessage = when {
|
||||
abiMismatch && is64bit -> LaunchUtils.LauncherError.LINKER_NEEDS_32BIT
|
||||
abiMismatch -> LaunchUtils.LauncherError.LINKER_NEEDS_64BIT
|
||||
e.message?.contains("__gxx_personality_v0") == true ->
|
||||
LaunchUtils.LauncherError.LINKER_FAILED_STL
|
||||
else -> LaunchUtils.LauncherError.LINKER_FAILED
|
||||
}
|
||||
|
||||
@@ -105,6 +122,13 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
mGLSurfaceView?.sendKeyBack()
|
||||
}
|
||||
})
|
||||
mGLSurfaceView?.manualBackEvents = true
|
||||
}
|
||||
|
||||
private fun createVersionFile() {
|
||||
@@ -146,11 +170,13 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME)
|
||||
|
||||
if (GamePackageUtils.getGameVersionCode(packageManager) >= 39L) {
|
||||
/*
|
||||
val customSymbols = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.CUSTOM_SYMBOL_LIST)
|
||||
if (customSymbols) {
|
||||
val symbolFile = File(LaunchUtils.getBaseDirectory(this), "exception_symbols.txt")
|
||||
LauncherFix.enableCustomSymbolList(symbolFile.path)
|
||||
}
|
||||
*/
|
||||
|
||||
// this fix requires geode v3, which is 2.206+
|
||||
// there is a short period in which 2.206 users will still have geode v2, but whatever. ig
|
||||
@@ -194,6 +220,9 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
val saveDir = LaunchUtils.getSaveDirectory(this)
|
||||
saveDir.mkdir()
|
||||
|
||||
// make sure it's loaded, just in case
|
||||
System.loadLibrary("c++_shared")
|
||||
|
||||
LauncherFix.loadLibrary()
|
||||
LauncherFix.setOriginalDataPath(Constants.GJ_DATA_DIR)
|
||||
LauncherFix.setDataPath(saveDir.path)
|
||||
@@ -203,22 +232,19 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
}
|
||||
|
||||
private fun setupPostLibraryLoad(packageInfo: PackageInfo) {
|
||||
FMOD.init(this)
|
||||
|
||||
// call native functions after native libraries init
|
||||
JniToCpp.setupHSSAssets(
|
||||
packageInfo.applicationInfo.sourceDir,
|
||||
packageInfo.applicationInfo!!.sourceDir,
|
||||
Environment.getExternalStorageDirectory().absolutePath
|
||||
)
|
||||
Cocos2dxHelper.nativeSetApkPath(packageInfo.applicationInfo.sourceDir)
|
||||
Cocos2dxHelper.nativeSetApkPath(packageInfo.applicationInfo!!.sourceDir)
|
||||
|
||||
BaseRobTopActivity.setCurrentActivity(this)
|
||||
registerReceiver()
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
||||
private fun tryLoadLibrary(packageInfo: PackageInfo, libraryName: String) {
|
||||
val nativeDir = getNativeLibraryDirectory(packageInfo.applicationInfo)
|
||||
val nativeDir = getNativeLibraryDirectory(packageInfo.applicationInfo!!)
|
||||
val libraryPath = if (nativeDir.endsWith('/')) "${nativeDir}lib$libraryName.so" else "$nativeDir/lib$libraryName.so"
|
||||
|
||||
try {
|
||||
@@ -365,15 +391,22 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
frameLayout.layoutParams = frameLayoutParams
|
||||
|
||||
if (displayMode == DisplayMode.LIMITED) {
|
||||
// despite not being perfectly 16:9, this is what Android calls "16:9"
|
||||
frameLayout.aspectRatio = 1.86f
|
||||
frameLayout.aspectRatio = mAspectRatio
|
||||
}
|
||||
|
||||
if (mScreenZoomFit && mScreenZoom != 1.0f) {
|
||||
frameLayout.fitZoom = true
|
||||
}
|
||||
|
||||
if (mScreenZoom != 1.0f) {
|
||||
frameLayout.zoom = mScreenZoom
|
||||
}
|
||||
|
||||
val editTextLayoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
val editText = Cocos2dxEditText(this)
|
||||
val editText = AppCompatEditText(this)
|
||||
editText.layoutParams = editTextLayoutParams
|
||||
frameLayout.addView(editText)
|
||||
|
||||
@@ -384,31 +417,33 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
|
||||
val showNotification = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.ENABLE_REDESIGN)
|
||||
if (showNotification) {
|
||||
val notificationView = ComposeView(this)
|
||||
frameLayout.addView(notificationView)
|
||||
val hasCards = determineDisplayedCards(this)
|
||||
if (hasCards.isNotEmpty()) {
|
||||
val notificationView = ComposeView(this)
|
||||
frameLayout.addView(notificationView)
|
||||
|
||||
notificationView.setContent {
|
||||
LaunchNotification()
|
||||
notificationView.setContent {
|
||||
LaunchNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glSurfaceView.setEGLContextClientVersion(2)
|
||||
glSurfaceView.setEGLConfigChooser(5, 6, 5, 0, 16, 8)
|
||||
|
||||
if (isAndroidEmulator()) {
|
||||
glSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0)
|
||||
}
|
||||
glSurfaceView.setEGLConfigChooser(EGLConfigChooser())
|
||||
|
||||
glSurfaceView.initView()
|
||||
|
||||
val renderer = Cocos2dxRenderer(glSurfaceView)
|
||||
glSurfaceView.setCocos2dxRenderer(renderer)
|
||||
|
||||
if (forceRefreshRate) {
|
||||
renderer.setFrameRate = true
|
||||
renderer.setFrameRate = mForceRefreshRate
|
||||
|
||||
val frameRate = mLimitedRefreshRate
|
||||
if (frameRate != null) {
|
||||
renderer.limitFrameRate(frameRate)
|
||||
}
|
||||
|
||||
editText.inputType = 145
|
||||
editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
||||
glSurfaceView.cocos2dxEditText = editText
|
||||
|
||||
return frameLayout
|
||||
@@ -417,14 +452,37 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
private fun setupUIState() {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
||||
|
||||
val preferenceUtils = PreferenceUtils.get(this)
|
||||
|
||||
displayMode = DisplayMode.fromInt(
|
||||
PreferenceUtils.get(this).getInt(PreferenceUtils.Key.DISPLAY_MODE)
|
||||
preferenceUtils.getInt(PreferenceUtils.Key.DISPLAY_MODE)
|
||||
)
|
||||
|
||||
forceRefreshRate = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.FORCE_HRR)
|
||||
mAspectRatio = ratioForPreference(preferenceUtils.getString(PreferenceUtils.Key.CUSTOM_ASPECT_RATIO) ?: "16_9")
|
||||
mScreenZoom = preferenceUtils.getInt(PreferenceUtils.Key.SCREEN_ZOOM) / 100.0f
|
||||
mScreenZoomFit = preferenceUtils.getBoolean(PreferenceUtils.Key.SCREEN_ZOOM_FIT)
|
||||
|
||||
val limitedRefreshRate = preferenceUtils.getInt(PreferenceUtils.Key.LIMIT_FRAME_RATE).takeIf { it != 0 }
|
||||
mLimitedRefreshRate = limitedRefreshRate
|
||||
|
||||
mForceRefreshRate = preferenceUtils.getBoolean(PreferenceUtils.Key.FORCE_HRR)
|
||||
if (mForceRefreshRate && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val chosenDisplay = display.supportedModes?.maxByOrNull { it.refreshRate }
|
||||
if (chosenDisplay != null) {
|
||||
var chosenRefreshRate = chosenDisplay.refreshRate
|
||||
println("setupUIState: Forcing a refresh rate of $chosenRefreshRate (display ${chosenDisplay.modeId})")
|
||||
|
||||
window.attributes.preferredRefreshRate = chosenRefreshRate
|
||||
} else {
|
||||
println("setupUIState could not find a display to select refresh rate of...")
|
||||
}
|
||||
}
|
||||
|
||||
if (displayMode == DisplayMode.FULLSCREEN && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
GeodeUtils.handleSafeArea = true
|
||||
} else {
|
||||
GeodeUtils.handleSafeArea = false
|
||||
}
|
||||
|
||||
hideSystemUi()
|
||||
@@ -452,19 +510,18 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
unregisterReceivers()
|
||||
FMOD.close()
|
||||
}
|
||||
|
||||
private fun resumeGame() {
|
||||
mIsRunning = true
|
||||
// Cocos2dxHelper.onResume()
|
||||
Cocos2dxHelper.onResume()
|
||||
mGLSurfaceView?.onResume()
|
||||
}
|
||||
|
||||
private fun pauseGame() {
|
||||
mIsRunning = false
|
||||
// Cocos2dxHelper.onPause()
|
||||
Cocos2dxHelper.onPause()
|
||||
mGLSurfaceView?.onPause()
|
||||
}
|
||||
|
||||
@@ -495,25 +552,6 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerReceiver() {
|
||||
unregisterReceivers()
|
||||
try {
|
||||
val filter = IntentFilter(Intent.ACTION_SCREEN_ON)
|
||||
filter.addAction(Intent.ACTION_SCREEN_OFF)
|
||||
filter.addAction(Intent.ACTION_USER_PRESENT)
|
||||
mReceiver = BaseRobTopActivity.ReceiverScreen()
|
||||
registerReceiver(mReceiver, filter)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterReceivers() {
|
||||
if (mReceiver != null) {
|
||||
unregisterReceiver(mReceiver)
|
||||
mReceiver = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun runOnGLThread(runnable: Runnable) {
|
||||
mGLSurfaceView?.queueEvent(runnable)
|
||||
}
|
||||
@@ -540,18 +578,6 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
private fun isAndroidEmulator(): Boolean {
|
||||
Log.d(sTag, "model=" + Build.MODEL)
|
||||
val product = Build.PRODUCT
|
||||
Log.d(sTag, "product=$product")
|
||||
var isEmulator = false
|
||||
if (product != null) {
|
||||
isEmulator = product == "sdk" || product.contains("_sdk") || product.contains("sdk_")
|
||||
}
|
||||
Log.d(sTag, "isEmulator=$isEmulator")
|
||||
return isEmulator
|
||||
}
|
||||
|
||||
override fun onCapabilityAdded(capability: String): Boolean {
|
||||
return when (capability) {
|
||||
GeodeUtils.CAPABILITY_EXTENDED_INPUT -> {
|
||||
@@ -562,6 +588,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
mGLSurfaceView?.sendTimestampEvents = true
|
||||
true
|
||||
}
|
||||
GeodeUtils.CAPABILITY_RICH_INPUT -> {
|
||||
mGLSurfaceView?.enableControllerEvents()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -575,7 +605,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
|
||||
val modListing = try {
|
||||
assets.list(internalModBase)
|
||||
} catch (ioe: IOException) {
|
||||
} catch (_: IOException) {
|
||||
emptyArray<String>()
|
||||
}
|
||||
|
||||
@@ -597,4 +627,78 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EGLConfigChooser : GLSurfaceView.EGLConfigChooser {
|
||||
// this comes from EGL14, but is unavailable on EGL10
|
||||
// also EGL14 is incompatible with EGL10. so whatever
|
||||
private var EGL_OPENGL_ES2_BIT: Int = 0x04
|
||||
|
||||
private data class ConfigValues(
|
||||
val redSize: Int,
|
||||
val greenSize: Int,
|
||||
val blueSize: Int,
|
||||
val alphaSize: Int,
|
||||
val depthSize: Int,
|
||||
val stencilSize: Int,
|
||||
)
|
||||
|
||||
private fun getAttribValue(egl: EGL10, display: EGLDisplay, config: EGLConfig, attrib: Int): Int {
|
||||
val value = IntArray(1)
|
||||
egl.eglGetConfigAttrib(display, config, attrib, value)
|
||||
|
||||
return value[0]
|
||||
}
|
||||
|
||||
private fun testConfig(egl: EGL10, display: EGLDisplay, attrib: ConfigValues): EGLConfig? {
|
||||
val attribList = intArrayOf(
|
||||
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
|
||||
EGL10.EGL_RED_SIZE, attrib.redSize,
|
||||
EGL10.EGL_GREEN_SIZE, attrib.greenSize,
|
||||
EGL10.EGL_BLUE_SIZE, attrib.blueSize,
|
||||
EGL10.EGL_ALPHA_SIZE, attrib.alphaSize,
|
||||
EGL10.EGL_DEPTH_SIZE, attrib.depthSize,
|
||||
EGL10.EGL_STENCIL_SIZE, attrib.stencilSize,
|
||||
EGL10.EGL_NONE
|
||||
)
|
||||
|
||||
val configs = arrayOfNulls<EGLConfig>(1)
|
||||
val numConfigs = IntArray(1)
|
||||
|
||||
val res = egl.eglChooseConfig(display, attribList, configs, configs.size, numConfigs)
|
||||
if (res && numConfigs[0] > 0 && configs[0] != null) {
|
||||
return configs[0]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun printConfig(egl: EGL10, display: EGLDisplay, config: EGLConfig) {
|
||||
val id = getAttribValue(egl, display, config, EGL10.EGL_CONFIG_ID)
|
||||
val red = getAttribValue(egl, display, config, EGL10.EGL_RED_SIZE)
|
||||
val green = getAttribValue(egl, display, config, EGL10.EGL_GREEN_SIZE)
|
||||
val blue = getAttribValue(egl, display, config, EGL10.EGL_BLUE_SIZE)
|
||||
val alpha = getAttribValue(egl, display, config, EGL10.EGL_ALPHA_SIZE)
|
||||
val depth = getAttribValue(egl, display, config, EGL10.EGL_DEPTH_SIZE)
|
||||
val stencil = getAttribValue(egl, display, config, EGL10.EGL_STENCIL_SIZE)
|
||||
println("EGLConfig $id: (red = $red, green = $green, blue = $blue, alpha = $alpha, depth = $depth, stencil = $stencil)")
|
||||
}
|
||||
|
||||
override fun chooseConfig(egl: EGL10, display: EGLDisplay): EGLConfig {
|
||||
val configs = listOf(
|
||||
ConfigValues(8, 8, 8, 8, 16, 8),
|
||||
ConfigValues(8, 8, 8, 0, 16, 8),
|
||||
ConfigValues(5, 6, 5, 0, 16, 8),
|
||||
ConfigValues(0, 0, 0, 0, 0, 0),
|
||||
)
|
||||
|
||||
return configs.firstNotNullOf {
|
||||
val config = testConfig(egl, display, it)
|
||||
if (config != null) {
|
||||
printConfig(egl, display, config)
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import com.geode.launcher.updater.ReleaseViewModel
|
||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||
import com.geode.launcher.ui.theme.LocalTheme
|
||||
import com.geode.launcher.ui.theme.Theme
|
||||
import com.geode.launcher.ui.theme.launcherTitleStyle
|
||||
import com.geode.launcher.utils.Constants
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
@@ -76,9 +77,10 @@ class MainActivity : ComponentActivity() {
|
||||
val theme = Theme.fromInt(themeOption)
|
||||
|
||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
@@ -111,7 +113,7 @@ fun MainScreen(
|
||||
val autoUpdateState by releaseViewModel.uiState.collectAsState()
|
||||
|
||||
val geodeJustInstalled = (autoUpdateState as? ReleaseViewModel.ReleaseUIState.Finished)
|
||||
?.hasUpdated ?: false
|
||||
?.hasUpdated == true
|
||||
val geodeInstalled = geodePreinstalled || geodeJustInstalled
|
||||
|
||||
var beginLaunch by remember { mutableStateOf(false) }
|
||||
@@ -159,11 +161,11 @@ fun MainScreen(
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.geode_logo),
|
||||
contentDescription = context.getString(R.string.launcher_logo_alt),
|
||||
modifier = Modifier.size(136.dp, 136.dp)
|
||||
modifier = Modifier.size(100.dp, 100.dp)
|
||||
)
|
||||
Text(
|
||||
context.getString(R.string.launcher_title),
|
||||
fontSize = 32.sp,
|
||||
style = launcherTitleStyle,
|
||||
modifier = Modifier.padding(12.dp)
|
||||
)
|
||||
|
||||
@@ -189,6 +191,10 @@ fun MainScreen(
|
||||
beginLaunch = true
|
||||
},
|
||||
)
|
||||
|
||||
if (gdVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
|
||||
UnsupportedVersionWarning()
|
||||
}
|
||||
}
|
||||
}
|
||||
gdInstalled -> {
|
||||
|
||||
@@ -40,7 +40,7 @@ class UserDirectoryProvider : DocumentsProvider() {
|
||||
|
||||
override fun onCreate(): Boolean {
|
||||
val context = context ?: return false
|
||||
rootDir = LaunchUtils.getBaseDirectory(context)
|
||||
rootDir = LaunchUtils.getBaseDirectory(context, true)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.geode.launcher.log
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.Instant
|
||||
|
||||
data class CrashDump @OptIn(ExperimentalTime::class) constructor(
|
||||
val filename: String,
|
||||
val lastModified: Instant
|
||||
)
|
||||
|
||||
class CrashViewModel(private val application: Application) : ViewModel() {
|
||||
companion object {
|
||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
|
||||
|
||||
CrashViewModel(
|
||||
application = application
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _lineState = mutableStateListOf<CrashDump>()
|
||||
val lineState = _lineState
|
||||
|
||||
private val _isLoading = MutableStateFlow(false)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
|
||||
private var failedToLoad = false
|
||||
|
||||
private var logJob: Job? = null
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
private fun getCrashList(): List<CrashDump> {
|
||||
val crashDirectory = LaunchUtils.getCrashDirectory(application)
|
||||
if (!crashDirectory.exists()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val children = crashDirectory.listFiles {
|
||||
// ignore indicator files (including old file)
|
||||
_, name -> name != LaunchUtils.CRASH_INDICATOR_NAME && name != "last-pid"
|
||||
} ?: return emptyList()
|
||||
|
||||
return children
|
||||
.map {
|
||||
CrashDump(
|
||||
filename = it.name,
|
||||
lastModified = Instant.fromEpochMilliseconds(it.lastModified())
|
||||
)
|
||||
}
|
||||
.sortedByDescending { it.lastModified }
|
||||
}
|
||||
|
||||
fun clearCrashes() {
|
||||
logJob?.cancel()
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val crashDirectory = LaunchUtils.getCrashDirectory(application)
|
||||
if (crashDirectory.exists()) {
|
||||
val files = crashDirectory.listFiles()
|
||||
files?.forEach {
|
||||
it.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_lineState.clear()
|
||||
}
|
||||
|
||||
private fun loadLogs() {
|
||||
_isLoading.value = true
|
||||
|
||||
logJob?.cancel()
|
||||
logJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
val lines = getCrashList()
|
||||
|
||||
_lineState.clear()
|
||||
_lineState.addAll(lines)
|
||||
|
||||
_isLoading.value = false
|
||||
|
||||
logJob = null
|
||||
}
|
||||
}
|
||||
|
||||
fun removeCrash(filename: String) {
|
||||
logJob?.cancel()
|
||||
|
||||
val crashPath = File(LaunchUtils.getCrashDirectory(application), filename)
|
||||
if (crashPath.exists()) {
|
||||
crashPath.delete()
|
||||
loadLogs()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
loadLogs()
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.geode.launcher.log
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toJavaInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import okio.Buffer
|
||||
import okio.BufferedSource
|
||||
import java.io.IOException
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.toJavaInstant
|
||||
|
||||
fun BufferedSource.readCChar(): Char {
|
||||
return this.readUtf8CodePoint().toChar()
|
||||
@@ -114,7 +116,7 @@ enum class LogPriority {
|
||||
/**
|
||||
* Represents a log entry from logcat.
|
||||
*/
|
||||
data class LogLine(
|
||||
data class LogLine @OptIn(ExperimentalTime::class) constructor(
|
||||
val process: ProcessInformation,
|
||||
val time: Instant,
|
||||
val logId: Int,
|
||||
@@ -133,6 +135,7 @@ data class LogLine(
|
||||
else -> throw IOException("LogLine::fromInputStream: unknown format for (headerSize = $size)")
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun fromBufferedSource(source: BufferedSource): LogLine {
|
||||
/*
|
||||
// from android <liblog/include/log/log_read.h>
|
||||
@@ -161,7 +164,7 @@ data class LogLine(
|
||||
};
|
||||
*/
|
||||
|
||||
/* val payloadLength = */ source.readShortLe()
|
||||
val payloadLength = source.readShortLe().toLong()
|
||||
val headerSize = source.readShortLe().toUShort()
|
||||
|
||||
val entryVersion = headerSizeToVersion(headerSize)
|
||||
@@ -177,11 +180,17 @@ data class LogLine(
|
||||
val processInformation = ProcessInformation(pid, tid, uid)
|
||||
val time = Instant.fromEpochSeconds(sec, nSec)
|
||||
|
||||
val priorityByte = source.readByte()
|
||||
// the payload is split into three parts
|
||||
// initial priority byte -> null terminated tag -> non null terminated message
|
||||
|
||||
val packetBuffer = Buffer()
|
||||
source.readFully(packetBuffer, payloadLength)
|
||||
|
||||
val priorityByte = packetBuffer.readByte()
|
||||
val priority = LogPriority.fromByte(priorityByte)
|
||||
|
||||
val tag = source.readCString()
|
||||
val message = source.readCString()
|
||||
val tag = packetBuffer.readCString()
|
||||
val message = packetBuffer.readUtf8()
|
||||
|
||||
return LogLine(
|
||||
process = processInformation,
|
||||
@@ -193,6 +202,7 @@ data class LogLine(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun showException(exception: Exception) = LogLine(
|
||||
process = ProcessInformation(0, 0, 0),
|
||||
time = Clock.System.now(),
|
||||
@@ -203,10 +213,14 @@ data class LogLine(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
val identifier = time.toJavaInstant()
|
||||
|
||||
val messageTrimmed by lazy { this.message.trim() }
|
||||
val messageTrimmed by lazy {
|
||||
this.message.trim { it <= ' ' }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
val formattedTime by lazy { this.time.toLocalDateTime(TimeZone.currentSystemDefault()) }
|
||||
val asSimpleString by lazy {
|
||||
"$formattedTime [${this.priority.toChar()}/${this.tag}]: ${this.messageTrimmed}"
|
||||
|
||||
@@ -9,7 +9,9 @@ import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.yield
|
||||
import okio.Buffer
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import java.io.EOFException
|
||||
@@ -61,7 +63,9 @@ class LogViewModel: ViewModel() {
|
||||
else "logcat -B -d"
|
||||
|
||||
val logProcess = try {
|
||||
Runtime.getRuntime().exec(logCommand)
|
||||
withContext(Dispatchers.IO) {
|
||||
Runtime.getRuntime().exec(logCommand)
|
||||
}
|
||||
} catch (ioe: IOException) {
|
||||
ioe.printStackTrace()
|
||||
logLines += LogLine.showException(ioe)
|
||||
@@ -71,13 +75,15 @@ class LogViewModel: ViewModel() {
|
||||
return logLines
|
||||
}
|
||||
|
||||
val logSource = logProcess.inputStream.source().buffer()
|
||||
// read entire log into a buffer so no logs are added to the buffer during processing
|
||||
val logBuffer = Buffer()
|
||||
logProcess.inputStream.source().buffer().readAll(logBuffer)
|
||||
|
||||
try {
|
||||
coroutineScope {
|
||||
// this runs until the stream is exhausted
|
||||
while (true) {
|
||||
val line = LogLine.fromBufferedSource(logSource)
|
||||
val line = LogLine.fromBufferedSource(logBuffer)
|
||||
|
||||
if (line.priority >= logLevel) {
|
||||
logLines += line
|
||||
|
||||
@@ -7,14 +7,18 @@ import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.AbsoluteRoundedCornerShape
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -65,4 +69,21 @@ fun StatusIndicator(
|
||||
text()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InlineDialog(headline: @Composable () -> Unit, body: @Composable () -> Unit, modifier: Modifier = Modifier) {
|
||||
ElevatedCard(modifier = modifier) {
|
||||
Column(modifier = Modifier.padding(18.dp).sizeIn(maxWidth = 512.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
) {
|
||||
headline()
|
||||
}
|
||||
|
||||
body()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
@@ -39,13 +38,16 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
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.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geode.launcher.preferences.ApplicationLogsActivity
|
||||
import com.geode.launcher.BuildConfig
|
||||
@@ -70,8 +72,20 @@ fun ErrorInfoBody(failureReason: LaunchUtils.LauncherError, modifier: Modifier =
|
||||
else -> stringResource(R.string.load_failed_generic_error_description)
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val recommendations = when (failureReason) {
|
||||
LaunchUtils.LauncherError.LINKER_FAILED -> listOf(
|
||||
LaunchUtils.LauncherError.LINKER_FAILED_STL -> {
|
||||
val isDeveloper = PreferenceUtils.get(context).getBoolean(PreferenceUtils.Key.DEVELOPER_MODE)
|
||||
|
||||
buildList {
|
||||
add(stringResource(R.string.load_failed_recommendation_update))
|
||||
add(stringResource(R.string.load_failed_recommendation_report))
|
||||
|
||||
if (isDeveloper)
|
||||
add(stringResource(R.string.load_failed_recommendation_dev_stl))
|
||||
}
|
||||
}
|
||||
LaunchUtils.LauncherError.LINKER_FAILED -> listOfNotNull(
|
||||
stringResource(R.string.load_failed_recommendation_reinstall),
|
||||
stringResource(R.string.load_failed_recommendation_update),
|
||||
)
|
||||
@@ -100,6 +114,7 @@ fun ErrorInfoTitle(failureReason: LaunchUtils.LauncherError) {
|
||||
val message = when (failureReason) {
|
||||
LaunchUtils.LauncherError.LINKER_NEEDS_64BIT,
|
||||
LaunchUtils.LauncherError.LINKER_NEEDS_32BIT,
|
||||
LaunchUtils.LauncherError.LINKER_FAILED_STL,
|
||||
LaunchUtils.LauncherError.LINKER_FAILED -> stringResource(R.string.load_failed_link_error)
|
||||
LaunchUtils.LauncherError.GENERIC -> stringResource(R.string.load_failed_generic_error)
|
||||
LaunchUtils.LauncherError.CRASHED -> stringResource(R.string.load_failed_crashed)
|
||||
@@ -182,7 +197,9 @@ fun ErrorInfoDescription(description: String, modifier: Modifier = Modifier) {
|
||||
Build.MODEL,
|
||||
Build.VERSION.RELEASE,
|
||||
currentRelease ?: "unknown",
|
||||
gameVersion
|
||||
gameVersion,
|
||||
LaunchUtils.applicationArchitecture,
|
||||
BuildConfig.VERSION_NAME
|
||||
),
|
||||
style = Typography.labelMedium
|
||||
)
|
||||
@@ -231,13 +248,18 @@ fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
|
||||
Text(stringResource(R.string.launcher_error_export_crash))
|
||||
}
|
||||
|
||||
FilledTonalButton(onClick = { onShowLogs(context) }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_description),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_error_view_logs))
|
||||
val developerSettingsEnabled = remember {
|
||||
PreferenceUtils.get(context).getBoolean(PreferenceUtils.Key.DEVELOPER_MODE)
|
||||
}
|
||||
if (developerSettingsEnabled) {
|
||||
FilledTonalButton(onClick = { onShowLogs(context) }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_description),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_error_view_logs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,14 +268,10 @@ fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
|
||||
|
||||
@Composable
|
||||
fun DownloadRecommendation(needsUniversal: Boolean, modifier: Modifier = Modifier) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val context = LocalContext.current
|
||||
val version = BuildConfig.VERSION_NAME
|
||||
|
||||
val showDownload = remember {
|
||||
!GamePackageUtils.isGameInstalled(context.packageManager) ||
|
||||
!GamePackageUtils.identifyGameLegitimacy(context.packageManager)
|
||||
}
|
||||
val showDownload = remember { GamePackageUtils.showDownloadBadge(context.packageManager) }
|
||||
|
||||
val downloadBase = "https://github.com/geode-sdk/android-launcher/releases/download/$version"
|
||||
val legacyDownloadUrl = "$downloadBase/geode-launcher-v$version-android32.apk"
|
||||
@@ -289,16 +307,20 @@ fun DownloadRecommendation(needsUniversal: Boolean, modifier: Modifier = Modifie
|
||||
|
||||
Text("\u2022\u00A0\u00A0$recommendationText")
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(downloadText),
|
||||
onClick = {
|
||||
uriHandler.openUri(downloadUrl)
|
||||
},
|
||||
|
||||
style = TextStyle.Default.copy(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textDecoration = TextDecoration.Underline
|
||||
),
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withLink(LinkAnnotation.Url(
|
||||
downloadUrl,
|
||||
TextLinkStyles(
|
||||
style = SpanStyle(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textDecoration = TextDecoration.Underline
|
||||
),
|
||||
)
|
||||
)) {
|
||||
append(downloadText)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,10 +279,7 @@ fun PlayButton(
|
||||
fun LaunchBlockedLabel(text: String) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val showDownload = remember {
|
||||
GamePackageUtils.isGameInstalled(context.packageManager) &&
|
||||
!GamePackageUtils.identifyGameLegitimacy(context.packageManager)
|
||||
}
|
||||
val showDownload = remember { GamePackageUtils.showDownloadBadge(context.packageManager) }
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(text, textAlign = TextAlign.Center, modifier = Modifier.padding(12.dp))
|
||||
@@ -292,3 +289,27 @@ fun LaunchBlockedLabel(text: String) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UnsupportedVersionWarning(modifier: Modifier = Modifier) {
|
||||
InlineDialog(
|
||||
headline = {
|
||||
Icon(
|
||||
Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(R.string.launcher_unsupported_version_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
},
|
||||
body = {
|
||||
Text(
|
||||
stringResource(R.string.launcher_game_update_required),
|
||||
)
|
||||
},
|
||||
modifier = modifier.padding(8.dp)
|
||||
)
|
||||
}
|
||||
@@ -26,7 +26,9 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
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.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -37,20 +39,27 @@ import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||
import com.geode.launcher.ui.theme.LocalTheme
|
||||
import com.geode.launcher.ui.theme.Theme
|
||||
import com.geode.launcher.updater.ReleaseManager
|
||||
import com.geode.launcher.utils.Constants
|
||||
import com.geode.launcher.utils.GamePackageUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
enum class LaunchNotificationType {
|
||||
GEODE_UPDATED, LAUNCHER_UPDATE_AVAILABLE, UPDATE_FAILED;
|
||||
GEODE_UPDATED, LAUNCHER_UPDATE_AVAILABLE, UPDATE_FAILED, UNSUPPORTED_VERSION;
|
||||
}
|
||||
|
||||
fun determineDisplayedCards(context: Context): List<LaunchNotificationType> {
|
||||
val cards = mutableListOf<LaunchNotificationType>()
|
||||
|
||||
if (GamePackageUtils.getGameVersionCode(context.packageManager) < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
|
||||
cards.add(LaunchNotificationType.UNSUPPORTED_VERSION)
|
||||
}
|
||||
|
||||
val releaseManager = ReleaseManager.get(context)
|
||||
val loadAutomatically = PreferenceUtils.get(context).getBoolean(PreferenceUtils.Key.LOAD_AUTOMATICALLY)
|
||||
|
||||
val availableUpdate = releaseManager.availableLauncherUpdate.value
|
||||
if (availableUpdate != null) {
|
||||
if (loadAutomatically && availableUpdate != null) {
|
||||
cards.add(LaunchNotificationType.LAUNCHER_UPDATE_AVAILABLE)
|
||||
}
|
||||
|
||||
@@ -69,29 +78,36 @@ fun determineDisplayedCards(context: Context): List<LaunchNotificationType> {
|
||||
|
||||
@Composable
|
||||
fun NotificationCardFromType(type: LaunchNotificationType) {
|
||||
val context = LocalContext.current
|
||||
|
||||
when (type) {
|
||||
LaunchNotificationType.LAUNCHER_UPDATE_AVAILABLE -> {
|
||||
var showInfoDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showInfoDialog) {
|
||||
LauncherUpdateInformation {
|
||||
showInfoDialog = false
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedNotificationCard(
|
||||
displayLength = 5000L,
|
||||
onClick = {
|
||||
installLauncherUpdate(context)
|
||||
showInfoDialog = true
|
||||
}
|
||||
) {
|
||||
LauncherUpdateContent()
|
||||
}
|
||||
}
|
||||
LaunchNotificationType.GEODE_UPDATED -> {
|
||||
AnimatedNotificationCard {
|
||||
UpdateNotificationContent()
|
||||
}
|
||||
}
|
||||
LaunchNotificationType.UPDATE_FAILED -> {
|
||||
AnimatedNotificationCard {
|
||||
UpdateFailedContent()
|
||||
}
|
||||
}
|
||||
LaunchNotificationType.UNSUPPORTED_VERSION -> {
|
||||
AnimatedNotificationCard {
|
||||
OutdatedVersionContent()
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +224,25 @@ fun UpdateNotificationContent(modifier: Modifier = Modifier) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OutdatedVersionContent(modifier: Modifier = Modifier) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = stringResource(id = R.string.launcher_notification_compatibility))
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.Warning,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(text = stringResource(id = R.string.launcher_notification_compatibility_description))
|
||||
},
|
||||
modifier = modifier.width(IntrinsicSize.Max)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateFailedContent(modifier: Modifier = Modifier) {
|
||||
ListItem(
|
||||
|
||||
@@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val COOLDOWN_LENGTH_MS = 1000L
|
||||
private const val COOLDOWN_LENGTH_MS = 3000L
|
||||
|
||||
class LaunchViewModel(private val application: Application): ViewModel() {
|
||||
companion object {
|
||||
@@ -65,12 +65,18 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
||||
GAME_MISSING,
|
||||
GAME_OUTDATED -> false
|
||||
}
|
||||
|
||||
fun isGameInstallIssue() = when (this) {
|
||||
GAME_OUTDATED,
|
||||
GAME_MISSING -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
sealed class LaunchUIState {
|
||||
data object Initial : LaunchUIState()
|
||||
data object UpdateCheck : LaunchUIState()
|
||||
data class Updating(val downloaded: Long, val outOf: Long) : LaunchUIState()
|
||||
data class Updating(val downloaded: Long, val outOf: Long?) : LaunchUIState()
|
||||
data class Cancelled(val reason: LaunchCancelReason, val inProgress: Boolean = false) : LaunchUIState()
|
||||
data object Working : LaunchUIState()
|
||||
data object Ready : LaunchUIState()
|
||||
@@ -156,7 +162,7 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
||||
return cancelLaunch(true)
|
||||
}
|
||||
|
||||
if (!readyTimerPassed) {
|
||||
if (!readyTimerPassed && !hasManuallyStarted) {
|
||||
return _uiState.emit(LaunchUIState.Working)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,39 +8,66 @@ import android.provider.DocumentsContract
|
||||
import android.text.format.Formatter
|
||||
import android.widget.Toast
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geode.launcher.R
|
||||
import com.geode.launcher.UserDirectoryProvider
|
||||
import com.geode.launcher.ui.theme.Typography
|
||||
import com.geode.launcher.updater.ReleaseManager
|
||||
import com.geode.launcher.updater.ReleaseViewModel
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import com.mikepenz.markdown.compose.LocalBulletListHandler
|
||||
import com.mikepenz.markdown.m3.Markdown
|
||||
import com.mikepenz.markdown.m3.markdownTypography
|
||||
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
|
||||
import java.io.File
|
||||
import java.net.ConnectException
|
||||
import java.net.UnknownHostException
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.toJavaInstant
|
||||
|
||||
fun clearDownloadedApks(context: Context) {
|
||||
// technically we should be using the activity results but it was too inconsistent for my liking
|
||||
@@ -72,10 +99,10 @@ fun generateInstallIntent(uri: Uri): Intent {
|
||||
|
||||
fun installLauncherUpdate(context: Context) {
|
||||
val nextUpdate = ReleaseManager.get(context).availableLauncherUpdate.value
|
||||
val launcherDownload = nextUpdate?.getLauncherDownload()
|
||||
val launcherDownload = nextUpdate?.getDownload()
|
||||
|
||||
if (launcherDownload != null) {
|
||||
val outputFile = launcherDownload.name
|
||||
val outputFile = launcherDownload.filename
|
||||
val baseDirectory = LaunchUtils.getBaseDirectory(context)
|
||||
|
||||
val outputPathFile = File(baseDirectory, outputFile)
|
||||
@@ -92,18 +119,116 @@ fun installLauncherUpdate(context: Context) {
|
||||
try {
|
||||
val intent = generateInstallIntent(uri)
|
||||
context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(context, context.getString(R.string.no_activity_found), Toast.LENGTH_SHORT).show()
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
// if it fails to install, just open it in the browser
|
||||
try {
|
||||
val downloadUrl = launcherDownload.url
|
||||
val downloadIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse(downloadUrl)
|
||||
}
|
||||
|
||||
context.startActivity(downloadIntent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(context, context.getString(R.string.no_activity_found), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, context.getString(R.string.release_fetch_no_releases), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class)
|
||||
@Composable
|
||||
fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val nextRelease = remember { ReleaseManager.get(context).availableLauncherUpdate.value }
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
|
||||
if (nextRelease != null) {
|
||||
ModalBottomSheet(onDismissRequest = { onDismiss() }, sheetState = sheetState) {
|
||||
Box(Modifier.verticalScroll(rememberScrollState())) {
|
||||
Column(modifier = Modifier.padding(horizontal = 24.dp)) {
|
||||
Text(
|
||||
stringResource(R.string.launcher_update_name, nextRelease.release.tagName),
|
||||
style = Typography.headlineSmall,
|
||||
)
|
||||
|
||||
val releasedTime = remember {
|
||||
DateFormat.getDateInstance().format(
|
||||
Date.from(nextRelease.release.publishedAt.toJavaInstant())
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
stringResource(R.string.launcher_update_released, "$releasedTime"),
|
||||
style = Typography.labelLarge
|
||||
)
|
||||
|
||||
Row(modifier = Modifier.padding(vertical = 12.dp)) {
|
||||
Button(
|
||||
onClick = { installLauncherUpdate(context) },
|
||||
) {
|
||||
Icon(painterResource(R.drawable.icon_download), contentDescription = null)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_install))
|
||||
}
|
||||
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
IconButton(onClick = { uriHandler.openUri(nextRelease.release.htmlUrl) }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_link),
|
||||
stringResource(R.string.launcher_update_external_link)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (nextRelease.release.body != null) {
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
|
||||
Text(
|
||||
stringResource(R.string.launcher_update_changelog),
|
||||
style = Typography.titleLarge
|
||||
)
|
||||
|
||||
// every new release of the markdown library adds another arg to it
|
||||
CompositionLocalProvider(LocalBulletListHandler provides { _, _, _, _, _ -> "• " }) {
|
||||
Markdown(
|
||||
content = nextRelease.release.body.replace("\r", ""),
|
||||
typography = markdownTypography(
|
||||
textLink = TextLinkStyles(
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
).toSpanStyle()
|
||||
),
|
||||
),
|
||||
flavour = GFMFlavourDescriptor(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var showInfoDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showInfoDialog) {
|
||||
LauncherUpdateInformation {
|
||||
showInfoDialog = false
|
||||
}
|
||||
}
|
||||
|
||||
ElevatedCard(modifier) {
|
||||
Column(
|
||||
// buttons add enough padding already, lower to compensate
|
||||
@@ -120,11 +245,18 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
|
||||
modifier = Modifier.padding(horizontal = 10.dp)
|
||||
)
|
||||
|
||||
TextButton(
|
||||
onClick = { installLauncherUpdate(context) },
|
||||
modifier = Modifier.align(Alignment.End)
|
||||
) {
|
||||
Text(stringResource(R.string.launcher_install))
|
||||
Row(modifier = Modifier.align(Alignment.End)) {
|
||||
TextButton(
|
||||
onClick = { showInfoDialog = true },
|
||||
) {
|
||||
Text(stringResource(R.string.launcher_update_view))
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = { installLauncherUpdate(context) },
|
||||
) {
|
||||
Text(stringResource(R.string.launcher_install))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,7 +274,7 @@ fun UpdateProgressIndicator(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(message)
|
||||
Text(message, style = MaterialTheme.typography.labelMedium)
|
||||
|
||||
Spacer(modifier = Modifier.padding(4.dp))
|
||||
|
||||
@@ -236,21 +368,26 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
|
||||
}
|
||||
|
||||
val outOf = remember(state.outOf) {
|
||||
Formatter.formatShortFileSize(context, state.outOf)
|
||||
state.outOf?.run {
|
||||
Formatter.formatShortFileSize(context, this)
|
||||
}
|
||||
}
|
||||
|
||||
UpdateProgressIndicator(
|
||||
stringResource(
|
||||
R.string.release_fetch_downloading,
|
||||
if (outOf != null) stringResource(
|
||||
R.string.release_fetch_downloading,
|
||||
downloaded,
|
||||
outOf
|
||||
) else stringResource(
|
||||
R.string.release_fetch_downloading_indeterminate,
|
||||
downloaded,
|
||||
outOf
|
||||
),
|
||||
modifier = modifier,
|
||||
releaseViewModel = releaseViewModel,
|
||||
progress = {
|
||||
val progress = state.downloaded / state.outOf.toDouble()
|
||||
val progress = state.downloaded / state.outOf!!.toDouble()
|
||||
progress.toFloat()
|
||||
}
|
||||
}.takeIf { state.outOf != null }
|
||||
)
|
||||
}
|
||||
is ReleaseViewModel.ReleaseUIState.InUpdateCheck -> {
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
package com.geode.launcher.preferences
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geode.launcher.R
|
||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||
import com.geode.launcher.ui.theme.LocalTheme
|
||||
import com.geode.launcher.ui.theme.Theme
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import com.geode.launcher.utils.ApplicationIcon
|
||||
import com.geode.launcher.utils.ApplicationIconDetails
|
||||
import com.geode.launcher.utils.IconUtils
|
||||
import com.geode.launcher.utils.adaptiveIconPainterResource
|
||||
|
||||
class ApplicationIconActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
|
||||
val theme = Theme.fromInt(themeOption)
|
||||
|
||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
ApplicationIconScreen(onBackPressedDispatcher)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onSelectIcon(context: Context, selectedIcon: ApplicationIconDetails) {
|
||||
val packageManager = context.packageManager
|
||||
|
||||
try {
|
||||
ApplicationIcon.entries.forEach {
|
||||
val iconData = IconUtils.getIconDetails(it)
|
||||
|
||||
packageManager.setComponentEnabledSetting(
|
||||
ComponentName(context, "${context.packageName}.${iconData.component}"),
|
||||
if (iconData.id == selectedIcon.id)
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||
PackageManager.DONT_KILL_APP
|
||||
)
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
Toast.makeText(context, R.string.application_icon_set_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
PreferenceUtils.get(context).setString(PreferenceUtils.Key.SELECTED_ICON, selectedIcon.id)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IconCard(iconData: ApplicationIconDetails, selected: Boolean, modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 1.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.then(modifier)
|
||||
.selectable(
|
||||
selected = selected,
|
||||
onClick = {
|
||||
onSelectIcon(context, iconData)
|
||||
},
|
||||
role = Role.RadioButton
|
||||
)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
Arrangement.SpaceBetween,
|
||||
Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
adaptiveIconPainterResource(iconData.iconId),
|
||||
contentDescription = null
|
||||
)
|
||||
Text(stringResource(iconData.nameId))
|
||||
}
|
||||
|
||||
RadioButton(
|
||||
selected = selected,
|
||||
onClick = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ApplicationIconScreen(
|
||||
onBackPressedDispatcher: OnBackPressedDispatcher?,
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
var showMoreOptions by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
Column {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back_icon_alt)
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.title_activity_application_icon),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
val selectedIcon by PreferenceUtils.useStringPreference(PreferenceUtils.Key.SELECTED_ICON)
|
||||
|
||||
Column(Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
) {
|
||||
ApplicationIcon.entries.forEach {
|
||||
val iconData = IconUtils.getIconDetails(it)
|
||||
|
||||
IconCard(iconData, selectedIcon == iconData.id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,7 @@ import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||
import com.geode.launcher.ui.theme.LocalTheme
|
||||
import com.geode.launcher.ui.theme.Theme
|
||||
import com.geode.launcher.ui.theme.Typography
|
||||
import com.geode.launcher.ui.theme.robotoMonoFamily
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -83,9 +84,10 @@ class ApplicationLogsActivity : ComponentActivity() {
|
||||
val theme = Theme.fromInt(themeOption)
|
||||
|
||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
@@ -135,12 +137,14 @@ fun SelectLogLevelDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onSelect = { onSelect(it) },
|
||||
initialValue = initialValue,
|
||||
toLabel = {
|
||||
// i'm lazy
|
||||
it.name.lowercase().replaceFirstChar { c -> c.uppercaseChar() }
|
||||
},
|
||||
optionsCount = LogPriority.FATAL downTo LogPriority.VERBOSE
|
||||
)
|
||||
) {
|
||||
(LogPriority.VERBOSE..LogPriority.FATAL).reversed().forEach {
|
||||
SelectOption(
|
||||
name = it.name.lowercase().replaceFirstChar { c -> c.uppercaseChar() },
|
||||
value = it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -164,12 +168,12 @@ fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
|
||||
"[${logLine.priority.toChar()}] ${logLine.formattedTime}",
|
||||
style = Typography.labelSmall,
|
||||
color = getLogColor(logLine.priority),
|
||||
fontFamily = FontFamily.Monospace
|
||||
fontFamily = robotoMonoFamily
|
||||
)
|
||||
|
||||
Text(
|
||||
"[${logLine.tag}]: ${logLine.messageTrimmed}",
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontFamily = robotoMonoFamily,
|
||||
style = Typography.bodyMedium
|
||||
)
|
||||
}
|
||||
@@ -240,21 +244,12 @@ fun ApplicationLogsScreen(
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
coroutineScope.launch {
|
||||
val data = logViewModel.getLogData()
|
||||
|
||||
val sendIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_TEXT, data)
|
||||
type = "text/plain"
|
||||
}
|
||||
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
|
||||
context.startActivity(shareIntent)
|
||||
saveLauncher.launch(context.getString(R.string.application_logs_default_filename))
|
||||
}
|
||||
}, enabled = !logLines.isEmpty()) {
|
||||
Icon(
|
||||
Icons.Filled.Share,
|
||||
contentDescription = stringResource(R.string.application_logs_share)
|
||||
painterResource(R.drawable.icon_save),
|
||||
contentDescription = stringResource(R.string.application_logs_export),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
|
||||
@@ -307,23 +302,6 @@ fun ApplicationLogsScreen(
|
||||
}
|
||||
)
|
||||
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_save),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.application_logs_export))
|
||||
},
|
||||
onClick = {
|
||||
saveLauncher.launch(context.getString(R.string.application_logs_default_filename))
|
||||
showMoreOptions = false
|
||||
},
|
||||
enabled = !logLines.isEmpty()
|
||||
)
|
||||
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
package com.geode.launcher.preferences
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.DocumentsContract
|
||||
import android.widget.Toast
|
||||
import java.text.DateFormat
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
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.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.geode.launcher.R
|
||||
import com.geode.launcher.UserDirectoryProvider
|
||||
import com.geode.launcher.log.CrashDump
|
||||
import com.geode.launcher.log.CrashViewModel
|
||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||
import com.geode.launcher.ui.theme.LocalTheme
|
||||
import com.geode.launcher.ui.theme.Theme
|
||||
import com.geode.launcher.ui.theme.Typography
|
||||
import com.geode.launcher.ui.theme.robotoMonoFamily
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.toJavaInstant
|
||||
|
||||
class CrashLogsActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
|
||||
val theme = Theme.fromInt(themeOption)
|
||||
|
||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
CrashLogsScreen(onBackPressedDispatcher)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun shareCrash(context: Context, filename: String) {
|
||||
val baseDirectory = LaunchUtils.getBaseDirectory(context)
|
||||
val crashPath = File(LaunchUtils.getCrashDirectory(context), filename)
|
||||
|
||||
if (!crashPath.exists()) {
|
||||
Toast.makeText(context, R.string.launcher_error_export_missing, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val providerLocation = crashPath.toRelativeString(baseDirectory)
|
||||
val documentsPath = "${UserDirectoryProvider.ROOT}${providerLocation}"
|
||||
|
||||
val uri = DocumentsContract.buildDocumentUri("${context.packageName}.user", documentsPath)
|
||||
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
type = "application/octet-stream"
|
||||
}
|
||||
|
||||
try {
|
||||
context.startActivity(shareIntent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.no_activity_found, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
@Composable
|
||||
fun CrashCard(crashDump: CrashDump, crashViewModel: CrashViewModel, modifier: Modifier = Modifier) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 1.dp, horizontal = 8.dp)
|
||||
.height(64.dp)
|
||||
.then(modifier)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
.padding(vertical = 8.dp, horizontal = 8.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceAround,
|
||||
modifier = Modifier.fillMaxHeight().offset(x = 12.dp).weight(0.7f)
|
||||
) {
|
||||
Text(
|
||||
crashDump.filename,
|
||||
fontFamily = robotoMonoFamily,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1
|
||||
)
|
||||
|
||||
val formattedLastModified = remember {
|
||||
DateFormat.getDateTimeInstance()
|
||||
.format(
|
||||
Date.from(crashDump.lastModified.toJavaInstant())
|
||||
)
|
||||
|
||||
}
|
||||
Text(formattedLastModified, style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.weight(0.3f), horizontalArrangement = Arrangement.End) {
|
||||
val context = LocalContext.current
|
||||
IconButton(onClick = {
|
||||
shareCrash(context, crashDump.filename)
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.Share,
|
||||
contentDescription = stringResource(R.string.application_logs_share)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
crashViewModel.removeCrash(crashDump.filename)
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
contentDescription = stringResource(R.string.application_crashes_delete_one)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CrashLogsScreen(
|
||||
onBackPressedDispatcher: OnBackPressedDispatcher?,
|
||||
crashViewModel: CrashViewModel = viewModel(factory = CrashViewModel.Factory)
|
||||
) {
|
||||
val logLines = crashViewModel.lineState
|
||||
val isLoading by crashViewModel.isLoading.collectAsState()
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
var showMoreOptions by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
Column {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back_icon_alt)
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
|
||||
Icon(
|
||||
Icons.Filled.MoreVert,
|
||||
contentDescription = stringResource(R.string.application_logs_more)
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showMoreOptions,
|
||||
onDismissRequest = { showMoreOptions = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_delete),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.application_logs_delete))
|
||||
}, onClick = {
|
||||
crashViewModel.clearCrashes()
|
||||
showMoreOptions = false
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.title_activity_crash_logs),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
if (!isLoading && logLines.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
stringResource(R.string.application_crashes_no_dumps),
|
||||
textAlign = TextAlign.Center,
|
||||
style = Typography.bodyLarge,
|
||||
modifier = Modifier.fillMaxSize().padding(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val itemsSize = logLines.size
|
||||
|
||||
itemsIndexed(
|
||||
logLines,
|
||||
key = { index, line -> line.filename }
|
||||
) { index, line ->
|
||||
val shape = RoundedCornerShape(
|
||||
topStart = if (index == 0) 16.dp else 4.dp,
|
||||
topEnd = if (index == 0) 16.dp else 4.dp,
|
||||
bottomStart = if (index + 1 == itemsSize) 16.dp else 4.dp,
|
||||
bottomEnd = if (index + 1 == itemsSize) 16.dp else 4.dp
|
||||
)
|
||||
|
||||
CrashCard(
|
||||
line,
|
||||
crashViewModel,
|
||||
modifier = Modifier.clip(shape)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showSystemUi = true)
|
||||
@Composable
|
||||
fun CrashLogsPreview() {
|
||||
GeodeLauncherTheme {
|
||||
CrashLogsScreen(null)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
package com.geode.launcher.preferences
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
@@ -31,6 +39,8 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
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
|
||||
@@ -38,7 +48,7 @@ import com.geode.launcher.R
|
||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||
import com.geode.launcher.ui.theme.LocalTheme
|
||||
import com.geode.launcher.ui.theme.Theme
|
||||
import com.geode.launcher.utils.LabelledText
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
|
||||
class DeveloperSettingsActivity : ComponentActivity() {
|
||||
@@ -51,9 +61,10 @@ class DeveloperSettingsActivity : ComponentActivity() {
|
||||
val theme = Theme.fromInt(themeOption)
|
||||
|
||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
@@ -66,11 +77,28 @@ class DeveloperSettingsActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun releaseChannelToKey(option: Int): String = when (option) {
|
||||
1 -> stringResource(R.string.preference_release_channel_beta)
|
||||
2 -> stringResource(R.string.preference_release_channel_nightly)
|
||||
else -> stringResource(R.string.preference_release_channel_stable)
|
||||
fun onOpenFolder(context: Context) {
|
||||
val file = LaunchUtils.getBaseDirectory(context)
|
||||
val clipboardManager =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboardManager.setPrimaryClip(
|
||||
ClipData.newPlainText(
|
||||
context.getString(R.string.export_folder_tag),
|
||||
file.path
|
||||
)
|
||||
)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.text_copied),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun onOpenLogs(context: Context) {
|
||||
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -99,6 +127,7 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.background),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -113,16 +142,12 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
|
||||
val developerMode by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
|
||||
|
||||
SettingsCard(
|
||||
title = stringResource(R.string.preference_developer_mode),
|
||||
preferenceKey = PreferenceUtils.Key.DEVELOPER_MODE,
|
||||
)
|
||||
|
||||
InlineText(
|
||||
stringResource(R.string.preference_developer_mode_about)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
OptionsGroup {
|
||||
SettingsCard(
|
||||
title = stringResource(R.string.preference_developer_mode),
|
||||
preferenceKey = PreferenceUtils.Key.DEVELOPER_MODE,
|
||||
)
|
||||
}
|
||||
|
||||
if (developerMode) {
|
||||
OptionsGroup(title = stringResource(R.string.preference_category_gameplay)) {
|
||||
@@ -135,31 +160,53 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(){}<>[]?:;'\"~`-_+=\\| ".contains(c)
|
||||
}}
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
SettingsCard(
|
||||
title = stringResource(R.string.preference_force_hrr),
|
||||
preferenceKey = PreferenceUtils.Key.FORCE_HRR,
|
||||
)
|
||||
}
|
||||
/*
|
||||
SettingsCard(
|
||||
title = stringResource(R.string.preference_override_exceptions_name),
|
||||
description = stringResource(R.string.preference_override_exceptions_description),
|
||||
preferenceKey = PreferenceUtils.Key.CUSTOM_SYMBOL_LIST
|
||||
)
|
||||
*/
|
||||
}
|
||||
|
||||
OptionsGroup(stringResource(R.string.preference_category_developer)) {
|
||||
val context = LocalContext.current
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preferences_view_logs),
|
||||
icon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_description),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = { onOpenLogs(context) }
|
||||
)
|
||||
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preferences_copy_external_button),
|
||||
description = LaunchUtils.getBaseDirectory(context).path,
|
||||
onClick = { onOpenFolder(context) }
|
||||
)
|
||||
}
|
||||
|
||||
OptionsGroup(title = stringResource(R.string.preference_category_updater)) {
|
||||
SettingsSelectCard(
|
||||
title = stringResource(R.string.preference_release_channel_tag_name),
|
||||
dialogTitle = stringResource(R.string.preference_release_channel_select),
|
||||
maxVal = 2,
|
||||
preferenceKey = PreferenceUtils.Key.RELEASE_CHANNEL_TAG,
|
||||
toLabel = { releaseChannelToKey(it) }
|
||||
)
|
||||
|
||||
ElevatedCard(modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)) {
|
||||
LabelledText(
|
||||
label = stringResource(R.string.preference_developer_update_notice),
|
||||
icon = {
|
||||
Icon(Icons.Filled.Info, null)
|
||||
},
|
||||
modifier = Modifier.padding(8.dp)
|
||||
options = linkedMapOf(
|
||||
0 to stringResource(R.string.preference_release_channel_stable),
|
||||
1 to stringResource(R.string.preference_release_channel_beta),
|
||||
2 to stringResource(R.string.preference_release_channel_nightly),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
OptionsGroup(title = stringResource(R.string.preference_category_testing)) {
|
||||
@@ -168,7 +215,14 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
preferenceKey = PreferenceUtils.Key.ENABLE_REDESIGN,
|
||||
)
|
||||
}
|
||||
|
||||
OptionsGroup(title = stringResource(R.string.preference_profiles)) {
|
||||
ProfileCreateCard()
|
||||
ProfileSelectCard()
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,12 @@ package com.geode.launcher.preferences
|
||||
|
||||
import android.app.UiModeManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.DocumentsContract
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
@@ -24,12 +23,14 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -50,11 +51,15 @@ import com.geode.launcher.updater.ReleaseViewModel
|
||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||
import com.geode.launcher.ui.theme.LocalTheme
|
||||
import com.geode.launcher.ui.theme.Theme
|
||||
import com.geode.launcher.ui.theme.Typography
|
||||
import com.geode.launcher.utils.ApplicationIcon
|
||||
import com.geode.launcher.utils.Constants
|
||||
import com.geode.launcher.utils.GamePackageUtils
|
||||
import com.geode.launcher.utils.IconUtils
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import java.net.ConnectException
|
||||
import java.net.UnknownHostException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class SettingsActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -66,9 +71,10 @@ class SettingsActivity : ComponentActivity() {
|
||||
val theme = Theme.fromInt(themeOption)
|
||||
|
||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
@@ -83,25 +89,6 @@ class SettingsActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onOpenFolder(context: Context) {
|
||||
val file = LaunchUtils.getBaseDirectory(context)
|
||||
val clipboardManager =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboardManager.setPrimaryClip(
|
||||
ClipData.newPlainText(
|
||||
context.getString(R.string.export_folder_tag),
|
||||
file.path
|
||||
)
|
||||
)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.text_copied),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun onOpenFileManager(context: Context) {
|
||||
// future thoughts: Intent.ACTION_VIEW may not work on some devices
|
||||
// the "correct" solution is to go through a list of possible intents/applications
|
||||
@@ -147,11 +134,16 @@ fun UpdateIndicator(
|
||||
is ReleaseViewModel.ReleaseUIState.InDownload -> {
|
||||
// is this the ideal design? idk
|
||||
enablePopup = true
|
||||
val progress = updateStatus.downloaded / updateStatus.outOf.toDouble()
|
||||
|
||||
CircularProgressIndicator(
|
||||
progress = { progress.toFloat() },
|
||||
)
|
||||
if (updateStatus.outOf != null) {
|
||||
CircularProgressIndicator(
|
||||
progress = {
|
||||
val progress = updateStatus.downloaded / updateStatus.outOf.toDouble()
|
||||
progress.toFloat()
|
||||
},
|
||||
)
|
||||
} else {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
@@ -178,9 +170,16 @@ fun UpdateIndicator(
|
||||
context.getString(R.string.preference_check_for_updates_success)
|
||||
)
|
||||
} else {
|
||||
snackbarHostState.showSnackbar(
|
||||
context.getString(R.string.preference_check_for_updates_none_found)
|
||||
)
|
||||
val gameVersion = GamePackageUtils.getGameVersionCodeOrNull(context.packageManager)
|
||||
if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE) {
|
||||
snackbarHostState.showSnackbar(
|
||||
context.getString(R.string.launcher_game_update_required_short)
|
||||
)
|
||||
} else {
|
||||
snackbarHostState.showSnackbar(
|
||||
context.getString(R.string.preference_check_for_updates_none_found)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
@@ -188,24 +187,6 @@ fun UpdateIndicator(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun themeToKey(theme: Int): String {
|
||||
return when (theme) {
|
||||
1 -> stringResource(R.string.preference_theme_light)
|
||||
2 -> stringResource(R.string.preference_theme_dark)
|
||||
else -> stringResource(R.string.preference_theme_default)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun displayOptionToKey(option: Int): String {
|
||||
return when (option) {
|
||||
1 -> stringResource(R.string.preference_display_mode_legacy)
|
||||
2 -> stringResource(R.string.preference_display_mode_fullscreen)
|
||||
else -> stringResource(R.string.preference_display_mode_default)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTheme(context: Context, theme: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
|
||||
@@ -227,16 +208,36 @@ fun updateTheme(context: Context, theme: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
fun onOpenLogs(context: Context) {
|
||||
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
fun onOpenDeveloperOptions(context: Context) {
|
||||
val launchIntent = Intent(context, DeveloperSettingsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeveloperModeDialog(onDismiss: () -> Unit, onEnable: () -> Unit) {
|
||||
AlertDialog(
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Filled.Warning,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
title = { Text(stringResource(R.string.preference_developer_options_dialog_title)) },
|
||||
text = { Text(stringResource(R.string.preference_developer_mode_about)) },
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.message_box_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onEnable) {
|
||||
Text(stringResource(R.string.message_box_accept))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
@@ -292,6 +293,7 @@ fun SettingsScreen(
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.background),
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
@@ -306,17 +308,53 @@ fun SettingsScreen(
|
||||
SettingsSelectCard(
|
||||
title = stringResource(R.string.preference_theme_name),
|
||||
dialogTitle = stringResource(R.string.preference_theme_select),
|
||||
maxVal = 2,
|
||||
preferenceKey = PreferenceUtils.Key.THEME,
|
||||
toLabel = { themeToKey(it) },
|
||||
options = linkedMapOf(
|
||||
0 to stringResource(R.string.preference_theme_default),
|
||||
1 to stringResource(R.string.preference_theme_light),
|
||||
2 to stringResource(R.string.preference_theme_dark),
|
||||
),
|
||||
extraSelectBehavior = { updateTheme(context, it) }
|
||||
)
|
||||
SettingsCard(
|
||||
title = stringResource(R.string.preference_black_background_name),
|
||||
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
SettingsCard(
|
||||
title = stringResource(R.string.preference_disable_styles),
|
||||
preferenceKey = PreferenceUtils.Key.DISABLE_USER_THEME
|
||||
)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val currentIcon by PreferenceUtils.useStringPreference(PreferenceUtils.Key.SELECTED_ICON)
|
||||
val iconName = remember(currentIcon) {
|
||||
IconUtils.getIconDetails(
|
||||
ApplicationIcon.fromId(currentIcon ?: "default")
|
||||
).nameId
|
||||
}
|
||||
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preference_set_application_icon),
|
||||
onClick = {
|
||||
val launchIntent = Intent(context, ApplicationIconActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
},
|
||||
description = stringResource(iconName),
|
||||
displayInline = true,
|
||||
)
|
||||
}
|
||||
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preferences_open_file_manager),
|
||||
icon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_folder),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = { onOpenFileManager(context) }
|
||||
)
|
||||
}
|
||||
@@ -330,17 +368,66 @@ fun SettingsScreen(
|
||||
SettingsSelectCard(
|
||||
title = stringResource(R.string.preference_display_mode_name),
|
||||
dialogTitle = stringResource(R.string.preference_display_mode_select),
|
||||
// necessary api doesn't exist on older versions of android
|
||||
maxVal = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) 2 else 1,
|
||||
preferenceKey = PreferenceUtils.Key.DISPLAY_MODE,
|
||||
toLabel = { displayOptionToKey(it) }
|
||||
options = buildMap {
|
||||
put(0, stringResource(R.string.preference_display_mode_default))
|
||||
put(1, stringResource(R.string.preference_display_mode_legacy))
|
||||
|
||||
// necessary api doesn't exist on older versions of android
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
put(2, stringResource(R.string.preference_display_mode_fullscreen))
|
||||
}
|
||||
}
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
SettingsCard(
|
||||
title = stringResource(R.string.preference_force_hrr),
|
||||
preferenceKey = PreferenceUtils.Key.FORCE_HRR,
|
||||
val currentDisplayMode by PreferenceUtils.useIntPreference(PreferenceUtils.Key.DISPLAY_MODE)
|
||||
|
||||
if (currentDisplayMode == 1) {
|
||||
SettingsStringSelectCard(
|
||||
title = stringResource(R.string.preference_custom_aspect_ratio_name),
|
||||
description = stringResource(R.string.preference_custom_aspect_ratio_description),
|
||||
dialogTitle = stringResource(R.string.preference_custom_aspect_ratio_select),
|
||||
preferenceKey = PreferenceUtils.Key.CUSTOM_ASPECT_RATIO,
|
||||
options = linkedMapOf(
|
||||
"16_9" to stringResource(R.string.preference_custom_aspect_ratio_16x9),
|
||||
"16_10" to stringResource(R.string.preference_custom_aspect_ratio_16x10),
|
||||
"4_3" to stringResource(R.string.preference_custom_aspect_ratio_4x3),
|
||||
"1_1" to stringResource(R.string.preference_custom_aspect_ratio_1x1),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
SettingsRangeCard(
|
||||
title = stringResource(R.string.preference_screen_zoom_name),
|
||||
description = stringResource(R.string.preference_screen_zoom_description),
|
||||
dialogTitle = stringResource(R.string.preference_screen_zoom_select),
|
||||
preferenceKey = PreferenceUtils.Key.SCREEN_ZOOM,
|
||||
labelSuffix = "x",
|
||||
range = 25..100,
|
||||
scale = 100,
|
||||
step = 5,
|
||||
) {
|
||||
SettingsCard(
|
||||
title = context.getString(R.string.preference_screen_zoom_fit_name),
|
||||
preferenceKey = PreferenceUtils.Key.SCREEN_ZOOM_FIT,
|
||||
asCard = false
|
||||
)
|
||||
}
|
||||
|
||||
val maxFrameRate = remember {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
context.display.supportedModes.maxOf { it.refreshRate }
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.refreshRate
|
||||
}.roundToInt()
|
||||
}
|
||||
|
||||
SettingsFPSCard(
|
||||
title = stringResource(R.string.preference_limit_framerate_name),
|
||||
dialogTitle = stringResource(R.string.preference_limit_framerate_select),
|
||||
preferenceKey = PreferenceUtils.Key.LIMIT_FRAME_RATE,
|
||||
maxFrameRate = maxFrameRate
|
||||
)
|
||||
}
|
||||
|
||||
OptionsGroup(context.getString(R.string.preference_category_updater)) {
|
||||
@@ -374,33 +461,105 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val developerModeEnabled by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
|
||||
|
||||
OptionsGroup(stringResource(R.string.preference_category_developer)) {
|
||||
if (developerModeEnabled) {
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preference_open_developer_options),
|
||||
icon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_data_object),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = { onOpenDeveloperOptions(context) }
|
||||
)
|
||||
}
|
||||
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preferences_view_logs),
|
||||
title = stringResource(R.string.preferences_view_crashes),
|
||||
icon = {
|
||||
Icon(painterResource(R.drawable.icon_description), contentDescription = null)
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_bug_report),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = { onOpenLogs(context) }
|
||||
)
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preference_open_developer_options),
|
||||
icon = {
|
||||
Icon(painterResource(R.drawable.icon_data_object), contentDescription = null)
|
||||
},
|
||||
onClick = { onOpenDeveloperOptions(context) }
|
||||
)
|
||||
OptionsButton(
|
||||
title = context.getString(R.string.preferences_copy_external_button),
|
||||
description = LaunchUtils.getBaseDirectory(context).path,
|
||||
onClick = { onOpenFolder(context) }
|
||||
onClick = {
|
||||
val launchIntent = Intent(context, CrashLogsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
stringResource(R.string.preference_launcher_version, BuildConfig.VERSION_NAME),
|
||||
style = Typography.labelSmall,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
)
|
||||
OptionsGroup(stringResource(R.string.preference_category_about)) {
|
||||
var launcherButtonTapped by remember { mutableIntStateOf(0) }
|
||||
var showDeveloperDialog by remember { mutableStateOf(false) }
|
||||
|
||||
OptionsButton(
|
||||
stringResource(R.string.preference_launcher_version_name),
|
||||
stringResource(R.string.preference_launcher_version_description, BuildConfig.VERSION_NAME),
|
||||
displayInline = true
|
||||
) {
|
||||
launcherButtonTapped += 1
|
||||
|
||||
if (launcherButtonTapped >= 7 && !developerModeEnabled) {
|
||||
showDeveloperDialog = true
|
||||
launcherButtonTapped = 0
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
if (showDeveloperDialog) {
|
||||
DeveloperModeDialog(
|
||||
onDismiss = {
|
||||
showDeveloperDialog = false
|
||||
},
|
||||
onEnable = {
|
||||
PreferenceUtils.get(context).setBoolean(PreferenceUtils.Key.DEVELOPER_MODE, true)
|
||||
showDeveloperDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
OptionsLabel(
|
||||
stringResource(R.string.preference_loader_version_name),
|
||||
stringResource(R.string.preference_loader_version_description, currentRelease ?: "unknown"),
|
||||
displayInline = true
|
||||
)
|
||||
|
||||
val gameInstalled = remember {
|
||||
GamePackageUtils.isGameInstalled(context.packageManager)
|
||||
}
|
||||
|
||||
if (gameInstalled) {
|
||||
val gameVersion = remember {
|
||||
GamePackageUtils.getUnifiedVersionName(context.packageManager)
|
||||
}
|
||||
|
||||
val gameSource = remember {
|
||||
context.getString(when (GamePackageUtils.identifyGameSource(context.packageManager)) {
|
||||
GamePackageUtils.GameSource.GOOGLE -> R.string.preference_game_source_google
|
||||
GamePackageUtils.GameSource.AMAZON -> R.string.preference_game_source_amazon
|
||||
else -> R.string.preference_game_source_unknown
|
||||
})
|
||||
}
|
||||
|
||||
OptionsLabel(
|
||||
stringResource(R.string.preference_game_version_name),
|
||||
stringResource(R.string.preference_game_version_description, gameVersion, gameSource),
|
||||
displayInline = true
|
||||
)
|
||||
|
||||
OptionsLabel(
|
||||
stringResource(R.string.preference_architecture_name),
|
||||
stringResource(R.string.preference_architecture_description, Build.MODEL, LaunchUtils.applicationArchitecture),
|
||||
displayInline = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
package com.geode.launcher.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.foundation.background
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
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.rememberScrollState
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -20,20 +30,30 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
@@ -42,6 +62,13 @@ import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||
import com.geode.launcher.ui.theme.Typography
|
||||
import com.geode.launcher.utils.LabelledText
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import com.geode.launcher.utils.Profile
|
||||
import com.geode.launcher.utils.ProfileManager
|
||||
import kotlin.math.log10
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
|
||||
fun toggleSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
|
||||
@@ -57,14 +84,22 @@ fun getSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OptionsGroup(title: String, content: @Composable () -> Unit) {
|
||||
fun OptionsGroup(title: String? = null, content: @Composable () -> Unit) {
|
||||
Column {
|
||||
Text(
|
||||
title,
|
||||
style = Typography.titleMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
)
|
||||
content()
|
||||
if (title != null) {
|
||||
Text(
|
||||
title,
|
||||
style = Typography.titleMedium,
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,9 +107,8 @@ fun OptionsGroup(title: String, content: @Composable () -> Unit) {
|
||||
fun SettingsSelectCard(
|
||||
title: String,
|
||||
dialogTitle: String,
|
||||
maxVal: Int,
|
||||
preferenceKey: PreferenceUtils.Key,
|
||||
toLabel: @Composable (Int) -> String,
|
||||
options: Map<Int, String>,
|
||||
extraSelectBehavior: ((Int) -> Unit)? = null
|
||||
) {
|
||||
val preferenceValue by PreferenceUtils.useIntPreference(preferenceKey)
|
||||
@@ -91,7 +125,7 @@ fun SettingsSelectCard(
|
||||
role = Role.Button
|
||||
)
|
||||
) {
|
||||
Text(toLabel(preferenceValue))
|
||||
Text(options[preferenceValue] ?: preferenceValue.toString())
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
@@ -109,9 +143,60 @@ fun SettingsSelectCard(
|
||||
extraSelectBehavior?.invoke(selected)
|
||||
},
|
||||
initialValue = preferenceValue,
|
||||
toLabel = toLabel,
|
||||
optionsCount = 0..maxVal
|
||||
)
|
||||
) {
|
||||
options.forEach { (k, v) ->
|
||||
SelectOption(name = v, value = k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsStringSelectCard(
|
||||
title: String,
|
||||
dialogTitle: String,
|
||||
preferenceKey: PreferenceUtils.Key,
|
||||
options: Map<String, String>,
|
||||
extraSelectBehavior: ((String?) -> Unit)? = null,
|
||||
description: String? = null,
|
||||
) {
|
||||
val preferenceValue by PreferenceUtils.useStringPreference(preferenceKey)
|
||||
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
OptionsCard(
|
||||
title = { OptionsTitle(title = title, description = description) },
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
onClick = {
|
||||
showDialog = true
|
||||
},
|
||||
role = Role.Button
|
||||
)
|
||||
) {
|
||||
Text(options[preferenceValue] ?: preferenceValue ?: "")
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
val context = LocalContext.current
|
||||
|
||||
SelectDialog(
|
||||
title = dialogTitle,
|
||||
onDismissRequest = {
|
||||
showDialog = false
|
||||
},
|
||||
onSelect = { selected ->
|
||||
showDialog = false
|
||||
PreferenceUtils.get(context)
|
||||
.setString(preferenceKey, selected)
|
||||
extraSelectBehavior?.invoke(selected)
|
||||
},
|
||||
initialValue = preferenceValue,
|
||||
) {
|
||||
options.forEach { (k, v) ->
|
||||
SelectOption(name = v, value = k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +262,7 @@ fun StringDialog(
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Ascii,
|
||||
autoCorrect = false
|
||||
autoCorrectEnabled = false
|
||||
),
|
||||
trailingIcon = {
|
||||
if (enteredValue.isNotEmpty()) {
|
||||
@@ -204,16 +289,531 @@ fun StringDialog(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RangeDialog(
|
||||
title: String,
|
||||
onDismissRequest: () -> Unit,
|
||||
onSelect: (Int) -> Unit,
|
||||
labelSuffix: String,
|
||||
initialValue: Int,
|
||||
range: IntRange,
|
||||
scale: Int,
|
||||
step: Int,
|
||||
children: @Composable () -> Unit
|
||||
) {
|
||||
var enteredValue by remember {
|
||||
mutableFloatStateOf(initialValue / scale.toFloat())
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDismissRequest() },
|
||||
title = {
|
||||
Text(title)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1.0f, false)
|
||||
) {
|
||||
val precision = log10(scale.toFloat()).roundToInt()
|
||||
|
||||
Slider(
|
||||
value = enteredValue,
|
||||
onValueChange = { enteredValue = it },
|
||||
valueRange = (range.first.toFloat()/scale)..(range.last.toFloat()/scale),
|
||||
steps = ((range.last - range.first) - 1) / step,
|
||||
modifier = Modifier.weight(1.0f)
|
||||
)
|
||||
|
||||
Text("%.${precision}f$labelSuffix".format(enteredValue))
|
||||
}
|
||||
|
||||
children()
|
||||
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onSelect((enteredValue * scale).roundToInt()) }) {
|
||||
Text(stringResource(R.string.message_box_accept))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.message_box_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FrameRateDialog(
|
||||
title: String,
|
||||
onDismissRequest: () -> Unit,
|
||||
onSelect: (Int) -> Unit,
|
||||
initialValue: Int,
|
||||
maxFrameRate: Int,
|
||||
) {
|
||||
var enteredValue by remember {
|
||||
mutableStateOf(
|
||||
if (initialValue == 0) maxFrameRate.toString()
|
||||
else initialValue.toString()
|
||||
)
|
||||
}
|
||||
|
||||
val minFrameRate = 5
|
||||
val currentValue = enteredValue.toIntOrNull()
|
||||
val maximumReached = currentValue != null && currentValue > maxFrameRate
|
||||
val minimumReached = currentValue != null && currentValue < minFrameRate
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDismissRequest() },
|
||||
title = {
|
||||
Text(title)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1.0f, false)
|
||||
) {
|
||||
IconButton(onClick = {
|
||||
val prevValue = max((currentValue ?: 0) - 5, minFrameRate)
|
||||
enteredValue = prevValue.toString()
|
||||
}, enabled = currentValue == null || currentValue > minFrameRate) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_remove),
|
||||
contentDescription = stringResource(R.string.preference_limit_framerate_subtract)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = enteredValue,
|
||||
onValueChange = {
|
||||
enteredValue = it
|
||||
},
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
suffix = {
|
||||
Text(stringResource(R.string.preference_limit_framerate_suffix))
|
||||
},
|
||||
label = {
|
||||
Text(stringResource(R.string.preference_limit_framerate_label))
|
||||
},
|
||||
isError = minimumReached || maximumReached,
|
||||
modifier = Modifier.weight(1.0f),
|
||||
)
|
||||
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
IconButton(onClick = {
|
||||
val nextValue = min((currentValue ?: 0) + 5, maxFrameRate)
|
||||
enteredValue = nextValue.toString()
|
||||
}, enabled = currentValue == null || currentValue < maxFrameRate) {
|
||||
Icon(
|
||||
Icons.Filled.Add,
|
||||
contentDescription = stringResource(R.string.preference_limit_framerate_add)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = { onSelect(0) }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_delete),
|
||||
contentDescription = stringResource(R.string.preference_limit_framerate_reset)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (minimumReached) {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
Text(
|
||||
stringResource(R.string.preference_limit_framerate_error_min, minFrameRate),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
} else if (maximumReached) {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.preference_limit_framerate_error_max,
|
||||
maxFrameRate
|
||||
),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
// automatically disable if already max
|
||||
if (currentValue == maxFrameRate) {
|
||||
onSelect(0)
|
||||
} else {
|
||||
onSelect(currentValue ?: 0)
|
||||
}
|
||||
},
|
||||
enabled = !minimumReached && !maximumReached && currentValue != null
|
||||
) {
|
||||
Text(stringResource(R.string.message_box_accept))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.message_box_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsFPSCard(
|
||||
title: String,
|
||||
dialogTitle: String,
|
||||
preferenceKey: PreferenceUtils.Key,
|
||||
maxFrameRate: Int,
|
||||
description: String? = null,
|
||||
) {
|
||||
var preferenceValue by PreferenceUtils.useIntPreference(preferenceKey)
|
||||
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
OptionsCard(
|
||||
title = { OptionsTitle(title = title, description = description) },
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
onClick = {
|
||||
showDialog = true
|
||||
},
|
||||
role = Role.Button
|
||||
)
|
||||
) {
|
||||
if (preferenceValue == 0) {
|
||||
Text(stringResource(R.string.preference_limit_framerate_default))
|
||||
} else {
|
||||
Text(stringResource(R.string.preference_limit_framerate_value, preferenceValue))
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
FrameRateDialog(
|
||||
title = dialogTitle,
|
||||
onDismissRequest = { showDialog = false },
|
||||
onSelect = {
|
||||
preferenceValue = it
|
||||
showDialog = false
|
||||
},
|
||||
initialValue = preferenceValue,
|
||||
maxFrameRate = maxFrameRate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsRangeCard(
|
||||
title: String,
|
||||
dialogTitle: String,
|
||||
preferenceKey: PreferenceUtils.Key,
|
||||
labelSuffix: String,
|
||||
range: IntRange,
|
||||
scale: Int,
|
||||
step: Int,
|
||||
description: String? = null,
|
||||
children: @Composable () -> Unit = {}
|
||||
) {
|
||||
var preferenceValue by PreferenceUtils.useIntPreference(preferenceKey)
|
||||
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
OptionsCard(
|
||||
title = {
|
||||
OptionsTitle(title = title, description = description)
|
||||
},
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
onClick = {
|
||||
showDialog = true
|
||||
},
|
||||
role = Role.Button
|
||||
)
|
||||
) {
|
||||
val precision = log10(scale.toFloat()).roundToInt()
|
||||
val scaledValue = preferenceValue / scale.toFloat()
|
||||
|
||||
Text("%.${precision}f$labelSuffix".format(scaledValue))
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
RangeDialog(
|
||||
title = dialogTitle,
|
||||
onDismissRequest = { showDialog = false },
|
||||
onSelect = {
|
||||
preferenceValue = it
|
||||
showDialog = false
|
||||
},
|
||||
initialValue = preferenceValue,
|
||||
range = range,
|
||||
scale = scale,
|
||||
labelSuffix = labelSuffix,
|
||||
step = step,
|
||||
children = children,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal val LocalSelectValue = compositionLocalOf<Any> { 0 }
|
||||
internal val LocalSelectSetValue = staticCompositionLocalOf<(Any) -> Unit> { {} }
|
||||
|
||||
@Composable
|
||||
fun <T> SelectOption(name: String, value: T, enabled: Boolean = true, leadingContent: @Composable (Boolean) -> Unit = {}) {
|
||||
val currentValue = LocalSelectValue.current
|
||||
val setValue = LocalSelectSetValue.current
|
||||
|
||||
val isSelected = currentValue.equals(value)
|
||||
|
||||
// do not give the row or column padding!! it messes up the selection effect
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
onClick = { setValue(value as Any) },
|
||||
role = Role.RadioButton,
|
||||
enabled = enabled
|
||||
)
|
||||
.padding(horizontal = 12.dp)
|
||||
) {
|
||||
Row(modifier = Modifier.weight(1.0f, true), verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = isSelected,
|
||||
onClick = { setValue(value as Any) },
|
||||
enabled = enabled
|
||||
)
|
||||
Text(name, style = Typography.bodyMedium)
|
||||
}
|
||||
|
||||
leadingContent(isSelected)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileCreateDialog(onDismissRequest: () -> Unit) {
|
||||
var enteredValue by remember { mutableStateOf("") }
|
||||
|
||||
val filename = enteredValue.take(16)
|
||||
.lowercase()
|
||||
.map {
|
||||
if ("qwertyuiopasdfghjklzxcvbnm1234567890-_.".contains(it))
|
||||
it else '_'
|
||||
}
|
||||
.joinToString("")
|
||||
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val minimumReached = enteredValue.isEmpty() || filename.isEmpty()
|
||||
|
||||
val profileManager = ProfileManager.get(context)
|
||||
val currentProfiles = remember {
|
||||
profileManager.getProfiles().map { it.path }.toSet()
|
||||
}
|
||||
|
||||
val isDuplicate = currentProfiles.contains(filename)
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDismissRequest() },
|
||||
title = {
|
||||
Text(stringResource(R.string.preference_profiles_create))
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1.0f, false)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = enteredValue,
|
||||
onValueChange = {
|
||||
enteredValue = it
|
||||
},
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
label = {
|
||||
Text(stringResource(R.string.preference_profiles_create_name))
|
||||
},
|
||||
isError = isDuplicate
|
||||
)
|
||||
}
|
||||
|
||||
if (isDuplicate) {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
Text(stringResource(R.string.preference_profiles_create_duplicate))
|
||||
} else if (!minimumReached) {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
Text(stringResource(R.string.preference_profiles_create_info, filename))
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
ProfileManager.get(context).storeProfile(Profile(filename, enteredValue))
|
||||
onDismissRequest()
|
||||
},
|
||||
enabled = !minimumReached && !isDuplicate
|
||||
) {
|
||||
Text(stringResource(R.string.preference_profiles_create_action))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.message_box_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileCreateCard() {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preference_profiles_create),
|
||||
icon = {
|
||||
Icon(painterResource(R.drawable.icon_person_add), contentDescription = null)
|
||||
}
|
||||
) {
|
||||
showDialog = true
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
ProfileCreateDialog(onDismissRequest = {
|
||||
showDialog = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileSelectCard() {
|
||||
val context = LocalContext.current
|
||||
val profileManager = ProfileManager.get(context)
|
||||
|
||||
val currentProfileId = remember {
|
||||
profileManager.getCurrentProfile()
|
||||
}
|
||||
|
||||
var currentProfileName = remember {
|
||||
val savedProfile = currentProfileId
|
||||
if (savedProfile == null) {
|
||||
"Default"
|
||||
} else {
|
||||
profileManager.getProfile(savedProfile)
|
||||
?.name ?: savedProfile
|
||||
}
|
||||
}
|
||||
|
||||
val clearedProfiles = remember { mutableStateListOf<String>() }
|
||||
|
||||
val profileList by profileManager.storedProfiles.collectAsState()
|
||||
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preference_profiles_select),
|
||||
description = stringResource(R.string.preference_profiles_current, currentProfileName)
|
||||
) {
|
||||
showDialog = true
|
||||
}
|
||||
|
||||
val activity = LocalActivity.current
|
||||
if (showDialog) {
|
||||
SelectDialog(
|
||||
title = stringResource(R.string.preference_profiles_select),
|
||||
onDismissRequest = {
|
||||
showDialog = false
|
||||
},
|
||||
onSelect = {
|
||||
showDialog = false
|
||||
|
||||
val selectedProfileId = it.takeIf { it.isNotEmpty() }
|
||||
|
||||
if (clearedProfiles.isNotEmpty()) {
|
||||
profileManager.deleteProfiles(clearedProfiles)
|
||||
}
|
||||
|
||||
if (currentProfileId != selectedProfileId) {
|
||||
profileManager
|
||||
.setCurrentProfile(selectedProfileId)
|
||||
|
||||
activity?.run {
|
||||
packageManager.getLaunchIntentForPackage(packageName)?.also {
|
||||
val mainIntent = Intent.makeRestartActivityTask(it.component)
|
||||
startActivity(mainIntent)
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
initialValue = currentProfileId ?: "",
|
||||
) {
|
||||
SelectOption("Default", "")
|
||||
|
||||
profileList.forEach { profile ->
|
||||
val path = profile.path
|
||||
|
||||
SelectOption(
|
||||
stringResource(R.string.preference_profiles_select_value, profile.name, path),
|
||||
path,
|
||||
enabled = !clearedProfiles.contains(path),
|
||||
leadingContent = { selected ->
|
||||
IconButton(onClick = {
|
||||
if (clearedProfiles.contains(path)) {
|
||||
clearedProfiles.remove(path)
|
||||
} else {
|
||||
clearedProfiles.add(path)
|
||||
}
|
||||
}, enabled = !selected) {
|
||||
if (clearedProfiles.contains(path)) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_undo),
|
||||
contentDescription = stringResource(R.string.preference_profiles_delete_undo_alt)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
contentDescription = stringResource(R.string.preference_profiles_delete_alt)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> SelectDialog(
|
||||
title: String,
|
||||
onDismissRequest: () -> Unit,
|
||||
onSelect: (T) -> Unit,
|
||||
initialValue: T,
|
||||
toLabel: @Composable (T) -> String,
|
||||
optionsCount: Iterable<T>,
|
||||
options: @Composable () -> Unit,
|
||||
) {
|
||||
var selectedValue by remember { mutableStateOf(initialValue) }
|
||||
val (selectedValue, setSelectedValue) = remember { mutableStateOf(initialValue) }
|
||||
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Card(
|
||||
@@ -221,7 +821,7 @@ fun <T> SelectDialog(
|
||||
) {
|
||||
// styling a dialog is actually a little hard if you're doing what i'm doing
|
||||
// maybe there's a better way to make these padding values...
|
||||
Column {
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text(
|
||||
title,
|
||||
style = Typography.titleLarge,
|
||||
@@ -232,27 +832,14 @@ fun <T> SelectDialog(
|
||||
)
|
||||
)
|
||||
|
||||
// do not give the row or column padding!! it messes up the selection effect
|
||||
optionsCount.forEach { id ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
onClick = { selectedValue = id },
|
||||
role = Role.RadioButton
|
||||
)
|
||||
.padding(horizontal = 12.dp)
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selectedValue == id,
|
||||
onClick = { selectedValue = id }
|
||||
)
|
||||
Text(
|
||||
toLabel(id),
|
||||
style = Typography.bodyMedium
|
||||
)
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
LocalSelectValue provides (selectedValue as Any),
|
||||
LocalSelectSetValue provides ({
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
setSelectedValue(it as T)
|
||||
})
|
||||
) {
|
||||
options()
|
||||
}
|
||||
|
||||
Row(
|
||||
@@ -279,22 +866,45 @@ fun <T> SelectDialog(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OptionsButton(title: String, description: String? = null, icon: (@Composable () -> Unit)? = null, onClick: () -> Unit) {
|
||||
fun OptionsButton(title: String, description: String? = null, icon: (@Composable () -> Unit)? = null, displayInline: Boolean = false, onClick: () -> Unit) {
|
||||
OptionsCard(
|
||||
title = {
|
||||
OptionsTitle(
|
||||
title = title,
|
||||
description = description,
|
||||
description = description.takeIf { !displayInline },
|
||||
icon = icon
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick, role = Role.Button)
|
||||
) { }
|
||||
.clickable(onClick = onClick, role = Role.Button),
|
||||
wrapContent = true
|
||||
) {
|
||||
if (displayInline && description != null) {
|
||||
Text(description, textAlign = TextAlign.End)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsCard(title: String, description: String? = null, icon: (@Composable () -> Unit)? = null, preferenceKey: PreferenceUtils.Key) {
|
||||
fun OptionsLabel(title: String, description: String? = null, icon: (@Composable () -> Unit)? = null, displayInline: Boolean = false) {
|
||||
OptionsCard(
|
||||
title = {
|
||||
OptionsTitle(
|
||||
title = title,
|
||||
description = description.takeIf { !displayInline },
|
||||
icon = icon
|
||||
)
|
||||
},
|
||||
wrapContent = true
|
||||
) {
|
||||
if (displayInline && description != null) {
|
||||
Text(description, textAlign = TextAlign.End)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsCard(title: String, description: String? = null, icon: (@Composable () -> Unit)? = null, asCard: Boolean = true, preferenceKey: PreferenceUtils.Key) {
|
||||
val context = LocalContext.current
|
||||
val settingEnabled = remember {
|
||||
mutableStateOf(getSetting(context, preferenceKey))
|
||||
@@ -303,19 +913,23 @@ fun SettingsCard(title: String, description: String? = null, icon: (@Composable
|
||||
OptionsCard(
|
||||
title = {
|
||||
OptionsTitle(
|
||||
Modifier.fillMaxWidth(0.75f),
|
||||
title = title,
|
||||
description = description,
|
||||
icon = icon
|
||||
)
|
||||
},
|
||||
modifier = Modifier.toggleable(
|
||||
modifier = if (asCard) Modifier.toggleable(
|
||||
value = settingEnabled.value,
|
||||
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceKey) },
|
||||
role = Role.Switch,
|
||||
)
|
||||
) else Modifier
|
||||
) {
|
||||
Switch(checked = settingEnabled.value, onCheckedChange = null)
|
||||
Switch(
|
||||
checked = settingEnabled.value,
|
||||
onCheckedChange = if (!asCard)
|
||||
{ _ -> settingEnabled.value = toggleSetting(context, preferenceKey) }
|
||||
else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,16 +956,28 @@ fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: Stri
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OptionsCard(modifier: Modifier = Modifier, title: @Composable () -> Unit, content: @Composable () -> Unit) {
|
||||
fun OptionsCard(modifier: Modifier = Modifier, wrapContent: Boolean = false, title: @Composable () -> Unit, content: @Composable () -> Unit) {
|
||||
Row(
|
||||
modifier
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.padding(vertical = 1.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.then(modifier)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.defaultMinSize(minHeight = 64.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
Arrangement.SpaceBetween,
|
||||
Alignment.CenterVertically,
|
||||
) {
|
||||
title()
|
||||
if (!wrapContent) {
|
||||
Box(modifier = Modifier.weight(1.0f)) {
|
||||
title()
|
||||
}
|
||||
Spacer(modifier = Modifier.size(12.dp))
|
||||
} else {
|
||||
title()
|
||||
}
|
||||
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.geode.launcher.ui.theme
|
||||
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.geode.launcher.R
|
||||
|
||||
val robotoMonoFamily = FontFamily(
|
||||
Font(R.font.robotomono_medium, FontWeight.Medium),
|
||||
Font(R.font.robotomono_regular, FontWeight.Normal)
|
||||
)
|
||||
|
||||
val quicksandFamily = FontFamily(
|
||||
Font(R.font.quicksand_semibold, FontWeight.SemiBold)
|
||||
)
|
||||
|
||||
val launcherTitleStyle = TextStyle(
|
||||
fontFamily = quicksandFamily,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 32.sp,
|
||||
letterSpacing = 0.sp
|
||||
)
|
||||
@@ -1,11 +1,12 @@
|
||||
package com.geode.launcher.updater
|
||||
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
@Serializable
|
||||
data class Asset(
|
||||
data class Asset @OptIn(ExperimentalTime::class) constructor(
|
||||
val url: String,
|
||||
val id: Int,
|
||||
val name: String,
|
||||
@@ -16,7 +17,7 @@ data class Asset(
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Release(
|
||||
data class Release @OptIn(ExperimentalTime::class) constructor(
|
||||
val url: String,
|
||||
val id: Int,
|
||||
val targetCommitish: String,
|
||||
@@ -24,40 +25,88 @@ class Release(
|
||||
val createdAt: Instant,
|
||||
val publishedAt: Instant,
|
||||
val assets: List<Asset>,
|
||||
val htmlUrl: String
|
||||
) {
|
||||
fun getDescription(): String {
|
||||
if (tagName == "nightly") {
|
||||
val htmlUrl: String,
|
||||
val body: String?,
|
||||
val name: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LoaderVersion @OptIn(ExperimentalTime::class) constructor(
|
||||
val tag: String,
|
||||
val version: String,
|
||||
val createdAt: Instant,
|
||||
val commitHash: String,
|
||||
val prerelease: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LoaderPayload<T>(
|
||||
val payload: T?,
|
||||
val error: String
|
||||
)
|
||||
|
||||
data class DownloadableAsset(val url: String, val filename: String, val size: Long? = null)
|
||||
|
||||
abstract class Downloadable {
|
||||
abstract fun getDescription(): String
|
||||
abstract fun getDescriptor(): Long
|
||||
abstract fun getDownload(): DownloadableAsset?
|
||||
}
|
||||
|
||||
class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadable() {
|
||||
override fun getDescription(): String {
|
||||
if (release.tagName == "nightly") {
|
||||
// get the commit from the assets
|
||||
// otherwise, a separate request is needed to get the hash (ew)
|
||||
val asset = assets.first()
|
||||
val asset = release.assets.first()
|
||||
val commit = asset.name.substring(6..12)
|
||||
|
||||
return "nightly-$commit"
|
||||
}
|
||||
|
||||
return tagName
|
||||
return release.tagName
|
||||
}
|
||||
|
||||
fun getDescriptor(): Long {
|
||||
return createdAt.epochSeconds
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override fun getDescriptor(): Long {
|
||||
return release.createdAt.epochSeconds
|
||||
}
|
||||
|
||||
fun getGeodeDownload(): Asset? {
|
||||
private fun getGitHubDownload(): Asset? {
|
||||
// try to find an asset that matches the architecture first
|
||||
val platform = LaunchUtils.platformName
|
||||
|
||||
val releaseSuffix = "$platform.zip"
|
||||
return assets.find {
|
||||
return release.assets.find {
|
||||
it.name.endsWith(releaseSuffix)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLauncherDownload(): Asset? {
|
||||
override fun getDownload(): DownloadableAsset? {
|
||||
val download = getGitHubDownload() ?: return null
|
||||
return DownloadableAsset(
|
||||
url = download.browserDownloadUrl,
|
||||
filename = download.name,
|
||||
size = download.size.toLong()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadableLauncherRelease(val release: Release) : Downloadable() {
|
||||
override fun getDescription(): String {
|
||||
return release.tagName
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override fun getDescriptor(): Long {
|
||||
return release.createdAt.epochSeconds
|
||||
}
|
||||
|
||||
private fun getGitHubDownload(): Asset? {
|
||||
val platform = LaunchUtils.platformName
|
||||
val use32BitPlatform = platform == "android32"
|
||||
|
||||
return assets.find { asset ->
|
||||
return release.assets.find { asset ->
|
||||
/* you know it's good when you pull out the truth table
|
||||
* u32bp | contains | found
|
||||
* 1 | 1 | 1
|
||||
@@ -70,4 +119,32 @@ class Release(
|
||||
(asset.name.contains("android32")) == use32BitPlatform
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDownload(): DownloadableAsset? {
|
||||
val download = getGitHubDownload() ?: return null
|
||||
return DownloadableAsset(
|
||||
url = download.browserDownloadUrl,
|
||||
filename = download.name,
|
||||
size = download.size.toLong()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadable() {
|
||||
override fun getDescription(): String {
|
||||
return version.tag
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override fun getDescriptor(): Long {
|
||||
return version.createdAt.epochSeconds
|
||||
}
|
||||
|
||||
override fun getDownload(): DownloadableAsset? {
|
||||
val filename = "geode-${version.tag}-${LaunchUtils.platformName}.zip"
|
||||
return DownloadableAsset(
|
||||
url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/$filename",
|
||||
filename = filename
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class ReleaseManager private constructor(
|
||||
sealed class ReleaseManagerState {
|
||||
data object InUpdateCheck : ReleaseManagerState()
|
||||
data class Failure(val exception: Exception) : ReleaseManagerState()
|
||||
data class InDownload(val downloaded: Long, val outOf: Long) : ReleaseManagerState()
|
||||
data class InDownload(val downloaded: Long, val outOf: Long?) : ReleaseManagerState()
|
||||
data class Finished(val hasUpdated: Boolean = false) : ReleaseManagerState()
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class ReleaseManager private constructor(
|
||||
val isInUpdate: Boolean
|
||||
get() = _uiState.value !is ReleaseManagerState.Failure && _uiState.value !is ReleaseManagerState.Finished
|
||||
|
||||
private val _availableLauncherUpdate = MutableStateFlow<Release?>(null)
|
||||
private val _availableLauncherUpdate = MutableStateFlow<DownloadableLauncherRelease?>(null)
|
||||
val availableLauncherUpdate = _availableLauncherUpdate.asStateFlow()
|
||||
|
||||
private fun sendError(e: Exception) {
|
||||
@@ -106,48 +106,48 @@ class ReleaseManager private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBestReleaseForGameVersion(): String? {
|
||||
private fun getBestReleaseForGameVersion(gameVersion: Long): String? = when {
|
||||
gameVersion >= 40L -> mapSelectedReleaseToTag()
|
||||
gameVersion == 39L -> "v3.9.3"
|
||||
gameVersion == 38L -> "v2.0.0-beta.27"
|
||||
gameVersion == 37L -> "v2.0.0-beta.4"
|
||||
else -> null
|
||||
}
|
||||
|
||||
private suspend fun getLatestRelease(): Downloadable? {
|
||||
if (!GamePackageUtils.isGameInstalled(applicationContext.packageManager)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val gameVersion = GamePackageUtils.getGameVersionCode(applicationContext.packageManager)
|
||||
val preferences = PreferenceUtils.get(applicationContext)
|
||||
|
||||
return when {
|
||||
gameVersion >= 39L -> mapSelectedReleaseToTag()
|
||||
gameVersion == 38L -> "v2.0.0-beta.27"
|
||||
gameVersion == 37L -> "v2.0.0-beta.5"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getLatestRelease(): Release? {
|
||||
val targetTag = getBestReleaseForGameVersion() ?: return null
|
||||
val targetTag = preferences.getInt(PreferenceUtils.Key.RELEASE_CHANNEL_TAG)
|
||||
|
||||
return when (targetTag) {
|
||||
TAG_LATEST -> releaseRepository.getLatestGeodeRelease()
|
||||
TAG_BETA -> releaseRepository.getLatestGeodePreRelease()
|
||||
else -> releaseRepository.getReleaseByTag(targetTag)
|
||||
2 -> releaseRepository.getReleaseByTag("nightly")
|
||||
1 -> releaseRepository.getLatestIndexRelease(gameVersion, true)
|
||||
else -> releaseRepository.getLatestIndexRelease(gameVersion, false)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun downloadLauncherUpdate(release: Release) {
|
||||
val download = release.getLauncherDownload() ?: return
|
||||
private suspend fun downloadLauncherUpdate(release: Downloadable) {
|
||||
val download = release.getDownload() ?: return
|
||||
|
||||
val outputDirectory = LaunchUtils.getBaseDirectory(applicationContext)
|
||||
val outputFile = File(outputDirectory, download.name)
|
||||
val outputFile = File(outputDirectory, download.filename)
|
||||
|
||||
if (outputFile.exists()) {
|
||||
// only download the apk once
|
||||
return
|
||||
}
|
||||
|
||||
_uiState.value = ReleaseManagerState.InDownload(0, download.size.toLong())
|
||||
_uiState.value = ReleaseManagerState.InDownload(0, download.size)
|
||||
|
||||
try {
|
||||
val fileStream = DownloadUtils.downloadStream(
|
||||
httpClient,
|
||||
download.browserDownloadUrl
|
||||
download.url
|
||||
) { progress, outOf ->
|
||||
_uiState.value = ReleaseManagerState.InDownload(progress, outOf)
|
||||
}
|
||||
@@ -159,8 +159,8 @@ class ReleaseManager private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun performUpdate(release: Release) {
|
||||
val releaseAsset = release.getGeodeDownload()
|
||||
private suspend fun performUpdate(release: Downloadable) {
|
||||
val releaseAsset = release.getDownload()
|
||||
if (releaseAsset == null) {
|
||||
val noAssetException = Exception("missing Android download")
|
||||
_uiState.value = ReleaseManagerState.Failure(noAssetException)
|
||||
@@ -169,33 +169,44 @@ class ReleaseManager private constructor(
|
||||
}
|
||||
|
||||
// set an initial download size
|
||||
_uiState.value = ReleaseManagerState.InDownload(0, releaseAsset.size.toLong())
|
||||
_uiState.value = ReleaseManagerState.InDownload(0, releaseAsset.size)
|
||||
|
||||
val outputFile = getTempFile()
|
||||
// clone the file instance as renameTo may move the original file
|
||||
val tempFilePath = outputFile.path
|
||||
|
||||
try {
|
||||
val fileStream = DownloadUtils.downloadStream(
|
||||
httpClient,
|
||||
releaseAsset.browserDownloadUrl
|
||||
releaseAsset.url
|
||||
) { progress, outOf ->
|
||||
_uiState.value = ReleaseManagerState.InDownload(progress, outOf)
|
||||
}
|
||||
|
||||
outputFile.parentFile?.mkdirs()
|
||||
|
||||
val geodeFile = getGeodeOutputPath()
|
||||
|
||||
DownloadUtils.extractFileFromZipStream(
|
||||
fileStream,
|
||||
outputFile.outputStream(),
|
||||
geodeFile.name
|
||||
)
|
||||
|
||||
// work around a permission issue from adb push
|
||||
if (geodeFile.exists()) {
|
||||
geodeFile.delete()
|
||||
}
|
||||
|
||||
geodeFile.parentFile?.mkdirs()
|
||||
|
||||
DownloadUtils.extractFileFromZipStream(
|
||||
fileStream,
|
||||
geodeFile.outputStream(),
|
||||
geodeFile.name
|
||||
)
|
||||
outputFile.renameTo(geodeFile)
|
||||
} catch (e: Exception) {
|
||||
sendError(e)
|
||||
return
|
||||
} finally {
|
||||
val tempFileClone = File(tempFilePath)
|
||||
try {
|
||||
if (tempFileClone.exists()) tempFileClone.delete()
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
|
||||
downloadLauncherUpdateIfNecessary()
|
||||
@@ -205,16 +216,14 @@ class ReleaseManager private constructor(
|
||||
_uiState.value = ReleaseManagerState.Finished(true)
|
||||
}
|
||||
|
||||
private fun fileWasExternallyModified(): Boolean {
|
||||
private fun fileWasExternallyModified(modifiedOnly: Boolean = false): Boolean {
|
||||
// make sure geode is still here. just in case
|
||||
val geodeFile = getGeodeOutputPath()
|
||||
if (!geodeFile.exists()) {
|
||||
return false
|
||||
return !modifiedOnly
|
||||
}
|
||||
|
||||
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
||||
if (!sharedPreferences.getBoolean(PreferenceUtils.Key.DEVELOPER_MODE)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val originalFileHash = sharedPreferences.getString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED)
|
||||
?: return false
|
||||
@@ -224,8 +233,8 @@ class ReleaseManager private constructor(
|
||||
return originalFileHash != currentFileHash
|
||||
}
|
||||
|
||||
private fun checkLauncherUpdate(launcherUpdate: Release) {
|
||||
if (launcherUpdate.tagName != BuildConfig.VERSION_NAME) {
|
||||
private fun checkLauncherUpdate(launcherUpdate: DownloadableLauncherRelease) {
|
||||
if (launcherUpdate.release.tagName != BuildConfig.VERSION_NAME) {
|
||||
_availableLauncherUpdate.value = launcherUpdate
|
||||
}
|
||||
}
|
||||
@@ -235,7 +244,7 @@ class ReleaseManager private constructor(
|
||||
downloadLauncherUpdate(update)
|
||||
}
|
||||
|
||||
private suspend fun checkForNewRelease(allowOverwriting: Boolean = false) {
|
||||
private suspend fun checkForNewRelease(isManual: Boolean = false) {
|
||||
val release = try {
|
||||
getLatestRelease()
|
||||
} catch (e: Exception) {
|
||||
@@ -266,19 +275,18 @@ class ReleaseManager private constructor(
|
||||
val currentVersion = sharedPreferences.getLong(PreferenceUtils.Key.CURRENT_VERSION_TIMESTAMP)
|
||||
val latestVersion = release.getDescriptor()
|
||||
|
||||
// make sure geode is still here. just in case
|
||||
val geodeFile = getGeodeOutputPath()
|
||||
|
||||
// check if an update is needed
|
||||
if (latestVersion == currentVersion && geodeFile.exists()) {
|
||||
if (latestVersion == currentVersion && !fileWasExternallyModified()) {
|
||||
downloadLauncherUpdateIfNecessary()
|
||||
|
||||
_uiState.value = ReleaseManagerState.Finished()
|
||||
return
|
||||
}
|
||||
|
||||
val allowOverwriting = !sharedPreferences.getBoolean(PreferenceUtils.Key.DEVELOPER_MODE) || isManual
|
||||
|
||||
// check if the file was externally modified
|
||||
if (!allowOverwriting && fileWasExternallyModified()) {
|
||||
if (!allowOverwriting && fileWasExternallyModified(true)) {
|
||||
downloadLauncherUpdateIfNecessary()
|
||||
|
||||
sendError(UpdateException(UpdateException.Reason.EXTERNAL_FILE_IN_USE))
|
||||
@@ -288,7 +296,7 @@ class ReleaseManager private constructor(
|
||||
performUpdate(release)
|
||||
}
|
||||
|
||||
private fun updatePreferences(release: Release) {
|
||||
private fun updatePreferences(release: Downloadable) {
|
||||
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
||||
|
||||
sharedPreferences.setString(
|
||||
@@ -318,6 +326,15 @@ class ReleaseManager private constructor(
|
||||
return File(geodeDirectory, geodeName)
|
||||
}
|
||||
|
||||
private fun getTempFile(): File {
|
||||
val geodeName = LaunchUtils.geodeFilename
|
||||
val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext)
|
||||
|
||||
val tempFile = File.createTempFile(geodeName, null, geodeDirectory)
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the current update job.
|
||||
*/
|
||||
@@ -343,4 +360,4 @@ class ReleaseManager private constructor(
|
||||
|
||||
return _uiState.asStateFlow()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
package com.geode.launcher.updater
|
||||
|
||||
import com.geode.launcher.utils.DownloadUtils.executeCoroutine
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.time.Clock
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.datetime.until
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNamingStrategy
|
||||
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
companion object {
|
||||
@@ -22,32 +24,34 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
|
||||
private const val GITHUB_RATELIMIT_REMAINING = "x-ratelimit-remaining"
|
||||
private const val GITHUB_RATELIMIT_RESET = "x-ratelimit-reset"
|
||||
|
||||
private const val GEODE_API_BASE = "https://api.geode-sdk.org/v1"
|
||||
}
|
||||
|
||||
suspend fun getLatestLauncherRelease(): Release? {
|
||||
suspend fun getLatestLauncherRelease(): DownloadableLauncherRelease? {
|
||||
val releasePath = "$GITHUB_API_BASE/repos/geode-sdk/android-launcher/releases/latest"
|
||||
|
||||
val url = URL(releasePath)
|
||||
|
||||
return getReleaseByUrl(url)
|
||||
return getReleaseByUrl(url)?.let(::DownloadableLauncherRelease)
|
||||
}
|
||||
|
||||
suspend fun getLatestGeodeRelease(): Release? {
|
||||
suspend fun getLatestGeodeRelease(): DownloadableGitHubLoaderRelease? {
|
||||
val releasePath = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases/latest"
|
||||
val url = URL(releasePath)
|
||||
|
||||
return getReleaseByUrl(url)
|
||||
return getReleaseByUrl(url)?.let(::DownloadableGitHubLoaderRelease)
|
||||
}
|
||||
|
||||
suspend fun getReleaseByTag(tag: String): Release? {
|
||||
suspend fun getReleaseByTag(tag: String): DownloadableGitHubLoaderRelease? {
|
||||
val releasePath = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases/tags/$tag"
|
||||
val url = URL(releasePath)
|
||||
|
||||
return getReleaseByUrl(url)
|
||||
return getReleaseByUrl(url)?.let(::DownloadableGitHubLoaderRelease)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
suspend fun getLatestGeodePreRelease(): Release? {
|
||||
@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class)
|
||||
suspend fun getLatestGeodePreRelease(): DownloadableGitHubLoaderRelease? {
|
||||
val releasesUrl = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases?per_page=2"
|
||||
val url = URL(releasesUrl)
|
||||
|
||||
@@ -71,7 +75,8 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
response.body!!.source()
|
||||
)
|
||||
|
||||
release.find { it.tagName != "nightly" }
|
||||
(release.find { it.tagName != "nightly" })
|
||||
?.let(::DownloadableGitHubLoaderRelease)
|
||||
}
|
||||
403 -> {
|
||||
// determine if the error code is a ratelimit
|
||||
@@ -100,6 +105,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
private fun generateRateLimitMessage(resetTime: Instant): String {
|
||||
val currentTime = Clock.System.now()
|
||||
val resetDelay = currentTime.until(resetTime, DateTimeUnit.SECOND)
|
||||
@@ -109,7 +115,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
return "api ratelimit reached, try again in ${formattedWait}"
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class)
|
||||
private suspend fun getReleaseByUrl(url: URL): Release? {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
@@ -137,8 +143,8 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
// determine if the error code is a ratelimit
|
||||
// (github docs say it sends 429 too, but haven't seen that)
|
||||
|
||||
val limitRemaining = response.headers.get(GITHUB_RATELIMIT_REMAINING)?.toInt()
|
||||
val limitReset = response.headers.get(GITHUB_RATELIMIT_RESET)?.toLong()
|
||||
val limitRemaining = response.headers[GITHUB_RATELIMIT_REMAINING]?.toInt()
|
||||
val limitReset = response.headers[GITHUB_RATELIMIT_RESET]?.toLong()
|
||||
|
||||
if (limitRemaining == 0 && limitReset != null) {
|
||||
// handle ratelimit with a custom error
|
||||
@@ -159,4 +165,45 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
suspend fun getLatestIndexRelease(gd: Long, prerelease: Boolean = false): DownloadableLoaderRelease? {
|
||||
val url = "$GEODE_API_BASE/loader/versions/latest".toHttpUrlOrNull()!!
|
||||
.newBuilder()
|
||||
.addQueryParameter("gd", gd.toString())
|
||||
.addQueryParameter("platform", "android")
|
||||
.addQueryParameter("prerelease", prerelease.toString())
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Accept", "application/json")
|
||||
.build()
|
||||
|
||||
val call = httpClient.newCall(request)
|
||||
val response = call.executeCoroutine()
|
||||
|
||||
return when (response.code) {
|
||||
200 -> {
|
||||
val format = Json {
|
||||
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
val body = format.decodeFromBufferedSource<LoaderPayload<LoaderVersion>>(
|
||||
response.body!!.source()
|
||||
)
|
||||
|
||||
val release = body.payload ?: throw IOException("index error: ${body.error}")
|
||||
|
||||
DownloadableLoaderRelease(release)
|
||||
}
|
||||
404 -> {
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
throw IOException("unknown response ${response.code}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
|
||||
sealed class ReleaseUIState {
|
||||
data object InUpdateCheck : ReleaseUIState()
|
||||
data class Failure(val exception: Exception) : ReleaseUIState()
|
||||
data class InDownload(val downloaded: Long, val outOf: Long) : ReleaseUIState()
|
||||
data class InDownload(val downloaded: Long, val outOf: Long?) : ReleaseUIState()
|
||||
data class Finished(val hasUpdated: Boolean = false) : ReleaseUIState()
|
||||
data class Cancelled(val isCancelling: Boolean = false) : ReleaseUIState()
|
||||
|
||||
|
||||
@@ -13,8 +13,14 @@ object Constants {
|
||||
@SuppressLint("SdCardPath")
|
||||
const val GJ_DATA_DIR = "/data/data/${PACKAGE_NAME}"
|
||||
|
||||
// anything below this version code is blocked from opening the game
|
||||
const val SUPPORTED_VERSION_CODE_MIN = 37L
|
||||
const val SUPPORTED_VERSION_CODE = 39L
|
||||
|
||||
const val SUPPORTED_VERSION_STRING = "2.2.142"
|
||||
// anything below this version code shows a warning on the main menu
|
||||
const val SUPPORTED_VERSION_CODE_MIN_WARNING = 40L
|
||||
|
||||
// anything above this version code will show a warning when starting the game
|
||||
const val SUPPORTED_VERSION_CODE = 40L
|
||||
|
||||
const val SUPPORTED_VERSION_STRING = "2.2.143"
|
||||
}
|
||||
@@ -2,26 +2,71 @@ package com.geode.launcher.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.FrameLayout
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.min
|
||||
|
||||
class ConstrainedFrameLayout(context: Context) : FrameLayout(context) {
|
||||
// avoid running the aspect ratio fix when we don't need it
|
||||
var aspectRatio: Float? = null;
|
||||
var aspectRatio: Float? = null
|
||||
var zoom: Float? = null
|
||||
var fitZoom: Boolean = false
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
if (aspectRatio != null) {
|
||||
var paddingX = 0.0f
|
||||
var paddingY = 0.0f
|
||||
|
||||
val currentZoom = zoom
|
||||
if (currentZoom != null) {
|
||||
val width = MeasureSpec.getSize(widthMeasureSpec)
|
||||
val height = MeasureSpec.getSize(heightMeasureSpec)
|
||||
if (width == 0 || height == 0) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
return
|
||||
}
|
||||
|
||||
val calculatedWidth = height * (aspectRatio ?: 1.0f);
|
||||
val padding = ((width - calculatedWidth) / 2).toInt()
|
||||
val calculatedWidth = width * currentZoom
|
||||
val calculatedHeight = height * currentZoom
|
||||
|
||||
// ignore paddings where the screen ends up looking odd
|
||||
if (padding > 0) {
|
||||
setPadding(padding, 0, padding, 0)
|
||||
paddingX = (width - calculatedWidth)/2
|
||||
paddingY = (height - calculatedHeight)/2
|
||||
}
|
||||
|
||||
val currentAspectRatio = aspectRatio
|
||||
if (currentAspectRatio != null) {
|
||||
val width = MeasureSpec.getSize(widthMeasureSpec)
|
||||
val height = MeasureSpec.getSize(heightMeasureSpec)
|
||||
if (width == 0 || height == 0) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
return
|
||||
}
|
||||
|
||||
val calculatedWidth = (height - paddingY * 2) * currentAspectRatio
|
||||
val padding = (width - calculatedWidth) / 2
|
||||
|
||||
paddingX = padding
|
||||
}
|
||||
|
||||
if (paddingX > 0.0 || paddingY > 0.0) {
|
||||
if (fitZoom) {
|
||||
val width = MeasureSpec.getSize(widthMeasureSpec)
|
||||
val height = MeasureSpec.getSize(heightMeasureSpec)
|
||||
|
||||
val calculatedHeight = (height - paddingY * 2)
|
||||
val calculatedWidth = (width - paddingX * 2)
|
||||
|
||||
val zoomFactor = min(height / calculatedHeight, width / calculatedWidth)
|
||||
|
||||
scaleX = zoomFactor
|
||||
scaleY = zoomFactor
|
||||
}
|
||||
|
||||
setPadding(
|
||||
ceil(paddingX).toInt(),
|
||||
ceil(paddingY).toInt(),
|
||||
floor(paddingX).toInt(),
|
||||
floor(paddingY).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
super.onMeasure(
|
||||
|
||||
@@ -20,6 +20,9 @@ object GamePackageUtils {
|
||||
}
|
||||
}
|
||||
|
||||
fun showDownloadBadge(packageManager: PackageManager) =
|
||||
!isGameInstalled(packageManager) || !identifyGameLegitimacy(packageManager)
|
||||
|
||||
fun getGameVersionCode(packageManager: PackageManager): Long {
|
||||
val game = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
|
||||
|
||||
@@ -31,15 +34,24 @@ object GamePackageUtils {
|
||||
}
|
||||
}
|
||||
|
||||
fun getGameVersionCodeOrNull(packageManager: PackageManager): Long? {
|
||||
return try {
|
||||
getGameVersionCode(packageManager)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getGameVersionString(packageManager: PackageManager): String {
|
||||
val game = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
|
||||
return game.versionName
|
||||
return game.versionName ?: "0.000"
|
||||
}
|
||||
|
||||
private val gameVersionMap = mapOf(
|
||||
37L to "2.200",
|
||||
38L to "2.205",
|
||||
39L to "2.206"
|
||||
39L to "2.206",
|
||||
40L to "2.2074" // ok
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -53,8 +65,8 @@ object GamePackageUtils {
|
||||
fun detectAbiMismatch(context: Context, packageInfo: PackageInfo, loadException: Error): Boolean {
|
||||
val abi = LaunchUtils.applicationArchitecture
|
||||
val isExtracted =
|
||||
packageInfo.applicationInfo.flags and ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS == ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS
|
||||
val isSplit = (packageInfo.applicationInfo.splitSourceDirs?.size ?: 0) > 1
|
||||
packageInfo.applicationInfo!!.flags and ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS == ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS
|
||||
val isSplit = (packageInfo.applicationInfo?.splitSourceDirs?.size ?: 0) > 1
|
||||
|
||||
val metadata =
|
||||
"Geometry Dash metadata:\nSplit sources: $isSplit\nExtracted libraries: $isExtracted\nLauncher architecture: $abi"
|
||||
@@ -74,7 +86,7 @@ object GamePackageUtils {
|
||||
val oppositeArchitecture = if (abi == "arm64-v8a") "armeabi-v7a" else "arm64-v8a"
|
||||
|
||||
// detect issues using native library directory (only works on extracted libraries)
|
||||
val nativeLibraryDir = packageInfo.applicationInfo.nativeLibraryDir
|
||||
val nativeLibraryDir = packageInfo.applicationInfo!!.nativeLibraryDir
|
||||
|
||||
if (nativeLibraryDir.contains(oppositeArchitecture)) {
|
||||
return true
|
||||
@@ -105,31 +117,47 @@ object GamePackageUtils {
|
||||
val clazz = assetManager.javaClass
|
||||
val aspMethod = clazz.getDeclaredMethod("addAssetPath", String::class.java)
|
||||
|
||||
aspMethod.invoke(assetManager, packageInfo.applicationInfo.sourceDir)
|
||||
packageInfo.applicationInfo.splitSourceDirs?.forEach {
|
||||
aspMethod.invoke(assetManager, packageInfo.applicationInfo?.sourceDir)
|
||||
packageInfo.applicationInfo?.splitSourceDirs?.forEach {
|
||||
aspMethod.invoke(assetManager, it)
|
||||
}
|
||||
}
|
||||
|
||||
private const val GAME_CERTIFICATE_HASH = "f5e7d8284d72c461a5f022d0cf755df101c3bb1e69cbe241bc1aef2cc5610a43"
|
||||
private const val AMAZON_CERTIFICATE_HASH = "1f228081e4d66e006887d4ba0f3d40e96a80c758b84231f11cd5c1c9aef6048f"
|
||||
|
||||
private fun validateCertificate(certificate: ByteArray): Boolean =
|
||||
certificate.toByteString().sha256().hex() == GAME_CERTIFICATE_HASH
|
||||
private fun validateCertificate(certificate: ByteArray): GameSource = when (val hash = certificate.toByteString().sha256().hex()) {
|
||||
GAME_CERTIFICATE_HASH -> GameSource.GOOGLE
|
||||
AMAZON_CERTIFICATE_HASH -> GameSource.AMAZON
|
||||
else -> {
|
||||
println("Encountered certificate hash: $hash")
|
||||
GameSource.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
fun identifyGameLegitimacy(packageManager: PackageManager): Boolean {
|
||||
enum class GameSource {
|
||||
UNKNOWN, GOOGLE, AMAZON
|
||||
}
|
||||
|
||||
fun identifyGameSource(packageManager: PackageManager): GameSource {
|
||||
@Suppress("DEPRECATION")
|
||||
val certificates = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val game = packageManager.getPackageInfo(Constants.PACKAGE_NAME, PackageManager.GET_SIGNING_CERTIFICATES)
|
||||
val signingInfo = game.signingInfo
|
||||
|
||||
signingInfo.signingCertificateHistory
|
||||
signingInfo?.signingCertificateHistory ?: return GameSource.UNKNOWN
|
||||
} else {
|
||||
val game = packageManager.getPackageInfo(Constants.PACKAGE_NAME, PackageManager.GET_SIGNATURES)
|
||||
game.signatures
|
||||
}
|
||||
|
||||
return certificates.any {
|
||||
return certificates?.map {
|
||||
validateCertificate(it.toByteArray())
|
||||
}
|
||||
}?.firstOrNull {
|
||||
it != GameSource.UNKNOWN
|
||||
} ?: GameSource.UNKNOWN
|
||||
}
|
||||
|
||||
fun identifyGameLegitimacy(packageManager: PackageManager) =
|
||||
identifyGameSource(packageManager) != GameSource.UNKNOWN
|
||||
}
|
||||
@@ -7,18 +7,27 @@ import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.hardware.lights.Light
|
||||
import android.hardware.lights.LightState
|
||||
import android.hardware.lights.LightsRequest
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.os.VibratorManager
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
|
||||
import android.util.Log
|
||||
import android.view.InputDevice
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.Keep
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.geode.launcher.BuildConfig
|
||||
import com.geode.launcher.R
|
||||
@@ -34,6 +43,8 @@ import kotlin.system.exitProcess
|
||||
@Suppress("unused", "KotlinJniMissingFunction")
|
||||
object GeodeUtils {
|
||||
private lateinit var activity: WeakReference<AppCompatActivity>
|
||||
var handleSafeArea: Boolean = false
|
||||
|
||||
private lateinit var openFileResultLauncher: ActivityResultLauncher<GeodeOpenFileActivityResult.OpenFileParams>
|
||||
private lateinit var openDirectoryResultLauncher: ActivityResultLauncher<Uri?>
|
||||
private lateinit var openFilesResultLauncher: ActivityResultLauncher<GeodeOpenFilesActivityResult.OpenFileParams>
|
||||
@@ -172,7 +183,7 @@ object GeodeUtils {
|
||||
val manager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = manager.primaryClip
|
||||
if (clip != null && clip.itemCount > 0) {
|
||||
return clip.getItemAt(0).text.toString()
|
||||
return clip.getItemAt(0).coerceToText(this).toString()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
@@ -373,11 +384,6 @@ object GeodeUtils {
|
||||
return activity.filesDir.canonicalPath
|
||||
}
|
||||
|
||||
private val gameVersionMap = mapOf(
|
||||
37L to "2.200",
|
||||
38L to "2.205"
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun getGameVersion(): String {
|
||||
// these versions should be aligned to windows releases, not what android says
|
||||
@@ -388,6 +394,14 @@ object GeodeUtils {
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current version code of the launcher.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getLauncherVersion(): Int {
|
||||
return BuildConfig.VERSION_CODE
|
||||
}
|
||||
|
||||
fun isGeodeUri(uri: Uri): Boolean {
|
||||
return "com.geode.launcher.user" == uri.authority
|
||||
}
|
||||
@@ -443,6 +457,68 @@ object GeodeUtils {
|
||||
|
||||
private external fun permissionCallback(granted: Boolean)
|
||||
|
||||
internal fun getVibrator(): Vibrator? = activity.get()?.run {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val manager = getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||
manager.defaultVibrator
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
}
|
||||
}
|
||||
|
||||
// vibration
|
||||
|
||||
/**
|
||||
* If vibration is supported on the device.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun vibrateSupported(): Boolean = getVibrator()?.hasVibrator() == true
|
||||
|
||||
/**
|
||||
* Vibrates for the given amount of time.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun vibrate(ms: Long) {
|
||||
if (ms == 0L) {
|
||||
getVibrator()?.cancel()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
getVibrator()?.vibrate(VibrationEffect.createOneShot(ms, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getVibrator()?.vibrate(ms)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vibrates with the given pattern.
|
||||
* @see android.os.VibrationEffect.createWaveform
|
||||
*/
|
||||
@JvmStatic
|
||||
fun vibratePattern(pattern: LongArray, repeat: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
getVibrator()?.vibrate(VibrationEffect.createWaveform(pattern, repeat))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getVibrator()?.vibrate(pattern, repeat)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getScreenInsets(): IntArray? {
|
||||
if (!handleSafeArea || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
return null
|
||||
}
|
||||
|
||||
return activity.get()?.run {
|
||||
val displayCutout = window.decorView.rootWindowInsets?.displayCutout ?: return null
|
||||
|
||||
intArrayOf(displayCutout.safeInsetLeft, displayCutout.safeInsetBottom, displayCutout.safeInsetRight, displayCutout.safeInsetTop)
|
||||
}
|
||||
}
|
||||
|
||||
const val ARGUMENT_SAFE_MODE = "--geode:safe-mode"
|
||||
|
||||
private var additionalLaunchArguments = arrayListOf<String>()
|
||||
@@ -474,6 +550,7 @@ object GeodeUtils {
|
||||
|
||||
const val CAPABILITY_EXTENDED_INPUT = "extended_input"
|
||||
const val CAPABILITY_TIMESTAMP_INPUT = "timestamp_inputs"
|
||||
const val CAPABILITY_RICH_INPUT = "internal_callbacks_v2"
|
||||
|
||||
private var capabilityListener: WeakReference<CapabilityListener?> = WeakReference(null)
|
||||
|
||||
@@ -481,13 +558,208 @@ object GeodeUtils {
|
||||
capabilityListener = WeakReference(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that the game client supports a feature.
|
||||
* These features typically require callbacks into the game, so ensure that the required methods are registered first.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun reportPlatformCapability(capability: String?): Boolean {
|
||||
if (capability.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return capabilityListener.get()?.onCapabilityAdded(capability) ?: false
|
||||
return capabilityListener.get()?.onCapabilityAdded(capability) == true
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun openWebview(url: String) {
|
||||
val activity = activity.get() ?: return
|
||||
CustomTabsIntent.Builder()
|
||||
.setShowTitle(true)
|
||||
.build()
|
||||
.launchUrl(activity, url.toUri())
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if any gamepad or joystick input devices are connected.
|
||||
* This is useful during start, but tracking inputDeviceAdded events is better during runtime.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun controllersConnected(): Int {
|
||||
val deviceIds = InputDevice.getDeviceIds()
|
||||
return deviceIds.count {
|
||||
InputDevice.getDevice(it)?.run {
|
||||
sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD || sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK
|
||||
} ?: false
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getConnectedDevices(): IntArray = InputDevice.getDeviceIds()
|
||||
|
||||
@JvmStatic
|
||||
fun getDevice(deviceId: Int): InputDevice? = InputDevice.getDevice(deviceId)
|
||||
|
||||
@JvmStatic
|
||||
fun getDeviceBatteryCapacity(deviceId: Int): Float {
|
||||
val device = InputDevice.getDevice(deviceId) ?: return 0.0f
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
device.batteryState.capacity
|
||||
} else {
|
||||
0.0f
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun deviceHasBattery(deviceId: Int): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
return false
|
||||
}
|
||||
|
||||
val device = InputDevice.getDevice(deviceId) ?: return false
|
||||
return device.batteryState.isPresent
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDeviceBatteryStatus(deviceId: Int): Int {
|
||||
val device = InputDevice.getDevice(deviceId) ?: return 0
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
device.batteryState.status
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDeviceLightsCount(deviceId: Int): Int {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
return 0
|
||||
}
|
||||
|
||||
val device = InputDevice.getDevice(deviceId) ?: return 0
|
||||
val lightsManager = device.lightsManager
|
||||
|
||||
return lightsManager.lights.size
|
||||
}
|
||||
|
||||
// this is based on paddleboat's api
|
||||
@JvmStatic
|
||||
fun getLightType(deviceId: Int): Int {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
return 0
|
||||
}
|
||||
|
||||
val device = InputDevice.getDevice(deviceId) ?: return 0
|
||||
val lightsManager = device.lightsManager
|
||||
|
||||
var flags = 0
|
||||
lightsManager.lights.forEach { light ->
|
||||
if (light.type == Light.LIGHT_TYPE_PLAYER_ID) {
|
||||
flags = flags or 1
|
||||
} else if (light.hasRgbControl()) {
|
||||
flags = flags or 2
|
||||
}
|
||||
}
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color for a device.
|
||||
* @return false if the given device does not exist or setting lights was otherwise unsuccessful
|
||||
*/
|
||||
@JvmStatic
|
||||
fun setDeviceLightColor(deviceId: Int, color: Int, type: Int): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
return false
|
||||
}
|
||||
|
||||
val device = InputDevice.getDevice(deviceId) ?: return false
|
||||
val lightsManager = device.lightsManager
|
||||
|
||||
val lights = lightsManager.lights
|
||||
if (lights.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val actedLights = lights.count { light ->
|
||||
val state = LightState.Builder()
|
||||
|
||||
if (light.type == Light.LIGHT_TYPE_PLAYER_ID && (type and 1 == 1)) {
|
||||
state.setPlayerId(color)
|
||||
} else if (light.hasRgbControl() && (type and 2 == 2)) {
|
||||
state.setColor(color)
|
||||
} else {
|
||||
return@count false
|
||||
}
|
||||
|
||||
val request = LightsRequest.Builder()
|
||||
.addLight(light, state.build())
|
||||
.build()
|
||||
|
||||
lightsManager.openSession()
|
||||
.requestLights(request)
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
return actedLights > 0
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDeviceHapticsCount(deviceId: Int): Int {
|
||||
val device = InputDevice.getDevice(deviceId) ?: return 0
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
return if (device.vibrator.hasVibrator()) 1 else 0
|
||||
}
|
||||
|
||||
val vibratorManager = device.vibratorManager
|
||||
val vibratorIds = vibratorManager.vibratorIds
|
||||
return vibratorIds.size
|
||||
}
|
||||
|
||||
private fun performVibration(vibrator: Vibrator?, durationMs: Long, intensity: Int) {
|
||||
vibrator?.apply {
|
||||
if (intensity == 0) {
|
||||
cancel()
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val vibrationEffect = VibrationEffect.createOneShot(durationMs, intensity)
|
||||
vibrate(vibrationEffect)
|
||||
} else {
|
||||
vibrate(durationMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vibrates a given device. Set motorIdx == -1 to vibrate all available motors on a device.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun vibrateDevice(deviceId: Int, durationMs: Long, intensity: Int, motorIdx: Int): Boolean {
|
||||
val device = InputDevice.getDevice(deviceId) ?: return false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val vibratorManager = device.vibratorManager
|
||||
val vibratorIds = vibratorManager.vibratorIds
|
||||
|
||||
if (motorIdx == -1) {
|
||||
vibratorIds.forEach {
|
||||
val vibrator = vibratorManager.getVibrator(it)
|
||||
performVibration(vibrator, durationMs, intensity)
|
||||
}
|
||||
} else {
|
||||
val deviceId = vibratorIds.getOrNull(motorIdx) ?: return false
|
||||
val vibrator = vibratorManager.getVibrator(deviceId)
|
||||
performVibration(vibrator, durationMs, intensity)
|
||||
}
|
||||
} else {
|
||||
val vibrator = device.vibrator
|
||||
performVibration(vibrator, durationMs, intensity)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
external fun nativeKeyUp(keyCode: Int, modifiers: Int)
|
||||
@@ -495,6 +767,24 @@ object GeodeUtils {
|
||||
external fun nativeActionScroll(scrollX: Float, scrollY: Float)
|
||||
external fun resizeSurface(width: Int, height: Int)
|
||||
|
||||
// represents the timestamp of the next input callback, in nanoseconds (most events don't send it, but it's there)
|
||||
/**
|
||||
* Gives the timestamp of the next input callback, in nanoseconds.
|
||||
* Enable this by sending the "timestamp_inputs" platform capability.
|
||||
* @see reportPlatformCapability
|
||||
*/
|
||||
external fun setNextInputTimestamp(timestamp: Long)
|
||||
|
||||
const val TOUCH_TYPE_BEGIN = 0
|
||||
const val TOUCH_TYPE_MOVE = 1
|
||||
const val TOUCH_TYPE_END = 2
|
||||
const val TOUCH_TYPE_CANCEL = 3
|
||||
|
||||
external fun internalKeyEvent(timestamp: Long, deviceId: Int, eventSource: Int, keyCode: Int, modifiers: Int, isDown: Boolean, repeatCount: Int)
|
||||
external fun internalTouchEvent(timestamp: Long, deviceId: Int, eventSource: Int, eventType: Int, ids: IntArray, xs: FloatArray, ys: FloatArray)
|
||||
external fun internalScrollEvent(timestamp: Long, deviceId: Int, eventSource: Int, scrollX: Float, scrollY: Float)
|
||||
external fun internalJoystickEvent(timestamp: Long, deviceId: Int, eventSource: Int, leftX: FloatArray, leftY: FloatArray, rightX: FloatArray, rightY: FloatArray, hatX: FloatArray, hatY: FloatArray, leftTrigger: FloatArray, rightTrigger: FloatArray)
|
||||
|
||||
external fun inputDeviceAdded(deviceId: Int)
|
||||
external fun inputDeviceChanged(deviceId: Int)
|
||||
external fun inputDeviceRemoved(deviceId: Int)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.geode.launcher.utils
|
||||
|
||||
import android.graphics.drawable.AdaptiveIconDrawable
|
||||
import android.os.Build
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import com.geode.launcher.R
|
||||
|
||||
// https://gist.github.com/tkuenneth/ddf598663f041dc79960cda503d14448?permalink_comment_id=4660486#gistcomment-4660486
|
||||
@Composable
|
||||
fun adaptiveIconPainterResource(@DrawableRes id: Int): Painter {
|
||||
val context = LocalContext.current
|
||||
val res = LocalResources.current
|
||||
val theme = context.theme
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val adaptiveIcon = remember(id) {
|
||||
ResourcesCompat.getDrawable(res, id, theme) as? AdaptiveIconDrawable
|
||||
}
|
||||
if (adaptiveIcon != null) {
|
||||
remember(id) {
|
||||
BitmapPainter(adaptiveIcon.toBitmap().asImageBitmap())
|
||||
}
|
||||
} else {
|
||||
painterResource(id)
|
||||
}
|
||||
} else {
|
||||
painterResource(id)
|
||||
}
|
||||
}
|
||||
|
||||
enum class ApplicationIcon {
|
||||
DEFAULT,
|
||||
GEODE,
|
||||
PRIDE,
|
||||
TRANS;
|
||||
|
||||
companion object {
|
||||
fun fromId(id: String) = when (id) {
|
||||
"default" -> ApplicationIcon.DEFAULT
|
||||
"geode" -> ApplicationIcon.GEODE
|
||||
"pride" -> ApplicationIcon.PRIDE
|
||||
"trans" -> ApplicationIcon.TRANS
|
||||
else -> ApplicationIcon.DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
fun toId() = when (this) {
|
||||
ApplicationIcon.PRIDE -> "pride"
|
||||
ApplicationIcon.GEODE -> "geode"
|
||||
ApplicationIcon.DEFAULT -> "default"
|
||||
ApplicationIcon.TRANS -> "trans"
|
||||
}
|
||||
}
|
||||
|
||||
data class ApplicationIconDetails(
|
||||
val id: String,
|
||||
val component: String,
|
||||
@param:DrawableRes val iconId: Int,
|
||||
@param:StringRes val nameId: Int,
|
||||
)
|
||||
|
||||
object IconUtils {
|
||||
fun getIconDetails(id: ApplicationIcon) = when (id) {
|
||||
ApplicationIcon.DEFAULT -> ApplicationIconDetails(
|
||||
id = id.toId(),
|
||||
component = "MainActivity",
|
||||
iconId = R.mipmap.ic_launcher,
|
||||
nameId = R.string.application_icon_default
|
||||
)
|
||||
ApplicationIcon.GEODE -> ApplicationIconDetails(
|
||||
id = id.toId(),
|
||||
component = "MainActivityGeode",
|
||||
iconId = R.mipmap.ic_launcher_geode,
|
||||
nameId = R.string.application_icon_geode
|
||||
)
|
||||
ApplicationIcon.PRIDE -> ApplicationIconDetails(
|
||||
id = id.toId(),
|
||||
component = "MainActivityPride",
|
||||
iconId = R.mipmap.ic_launcher_pride,
|
||||
nameId = R.string.application_icon_pride
|
||||
)
|
||||
ApplicationIcon.TRANS -> ApplicationIconDetails(
|
||||
id = id.toId(),
|
||||
component = "MainActivityTrans",
|
||||
iconId = R.mipmap.ic_launcher_trans,
|
||||
nameId = R.string.application_icon_trans
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.geode.launcher.utils
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import java.io.File
|
||||
import java.io.Serializable
|
||||
|
||||
object LaunchUtils {
|
||||
// supposedly CPU_ABI returns the current arch for the running application
|
||||
@@ -46,7 +47,7 @@ object LaunchUtils {
|
||||
/**
|
||||
* Returns the directory that Geode/the game should base itself off of.
|
||||
*/
|
||||
fun getBaseDirectory(context: Context): File {
|
||||
fun getBaseDirectory(context: Context, ignoreProfile: Boolean = false): File {
|
||||
// deprecated, but seems to be the best choice of directory (i forced mat to test it)
|
||||
// also, is getting the first item the correct choice here?? what do they mean
|
||||
@Suppress("DEPRECATION")
|
||||
@@ -57,6 +58,14 @@ object LaunchUtils {
|
||||
val noMediaPath = File(dir, ".nomedia")
|
||||
noMediaPath.createNewFile()
|
||||
|
||||
val currentProfile = ProfileManager.get(context).getCurrentProfile()
|
||||
if (currentProfile != null && !ignoreProfile) {
|
||||
val profile = File(dir, "profiles/$currentProfile/")
|
||||
profile.mkdirs()
|
||||
|
||||
return profile
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
@@ -64,9 +73,9 @@ object LaunchUtils {
|
||||
return File(getBaseDirectory(context), "save")
|
||||
}
|
||||
|
||||
private const val CRASH_INDICATOR_NAME = "lastSessionDidCrash"
|
||||
const val CRASH_INDICATOR_NAME = "lastSessionDidCrash"
|
||||
|
||||
private fun getCrashDirectory(context: Context): File {
|
||||
fun getCrashDirectory(context: Context): File {
|
||||
val base = getBaseDirectory(context)
|
||||
return File(base, "game/geode/crashlogs/")
|
||||
}
|
||||
@@ -92,9 +101,10 @@ object LaunchUtils {
|
||||
return crashIndicatorFile.exists()
|
||||
}
|
||||
|
||||
enum class LauncherError {
|
||||
enum class LauncherError : Serializable {
|
||||
LINKER_NEEDS_64BIT,
|
||||
LINKER_NEEDS_32BIT,
|
||||
LINKER_FAILED_STL,
|
||||
LINKER_FAILED,
|
||||
GENERIC,
|
||||
CRASHED;
|
||||
|
||||
@@ -8,7 +8,7 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
@@ -20,8 +20,6 @@ import com.geode.launcher.BuildConfig
|
||||
*/
|
||||
class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
companion object {
|
||||
private const val FILE_KEY = "GeodeLauncherPreferencesFileKey"
|
||||
|
||||
@Composable
|
||||
fun useBooleanPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Boolean> {
|
||||
return usePreference(
|
||||
@@ -70,11 +68,14 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
preferenceSet: (PreferenceUtils, Key, T) -> Unit
|
||||
): MutableState<T> {
|
||||
val context = LocalContext.current
|
||||
val sharedPreferences = context.getSharedPreferences(FILE_KEY, Context.MODE_PRIVATE)
|
||||
|
||||
val currentProfile = remember { ProfileManager.get(context).getCurrentFileKey() }
|
||||
val sharedPreferences = context.getSharedPreferences(currentProfile, Context.MODE_PRIVATE)
|
||||
|
||||
val preferences = get(sharedPreferences)
|
||||
|
||||
val preferenceValue = remember {
|
||||
// inferring the type causes an internal compiler error. lol
|
||||
val preferenceValue: MutableState<T> = remember {
|
||||
val state = mutableStateOf(preferenceGet(preferences, preferenceKey))
|
||||
object : MutableState<T> by state {
|
||||
override var value: T
|
||||
@@ -113,7 +114,8 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
}
|
||||
|
||||
fun get(context: Context): PreferenceUtils {
|
||||
val sharedPreferences = context.getSharedPreferences(FILE_KEY, Context.MODE_PRIVATE)
|
||||
val currentFileKey = ProfileManager.get(context).getCurrentFileKey()
|
||||
val sharedPreferences = context.getSharedPreferences(currentFileKey, Context.MODE_PRIVATE)
|
||||
return get(sharedPreferences)
|
||||
}
|
||||
|
||||
@@ -141,7 +143,13 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
RELEASE_CHANNEL_TAG,
|
||||
DEVELOPER_MODE,
|
||||
CLEANUP_APKS,
|
||||
CUSTOM_SYMBOL_LIST
|
||||
CUSTOM_SYMBOL_LIST,
|
||||
CUSTOM_ASPECT_RATIO,
|
||||
SCREEN_ZOOM,
|
||||
SCREEN_ZOOM_FIT,
|
||||
LIMIT_FRAME_RATE,
|
||||
DISABLE_USER_THEME,
|
||||
SELECTED_ICON
|
||||
}
|
||||
|
||||
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
||||
@@ -154,10 +162,17 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
|
||||
private fun defaultValueForIntKey(key: Key) = when (key) {
|
||||
Key.DISPLAY_MODE -> if (this.getBoolean(Key.LIMIT_ASPECT_RATIO)) 1 else 0
|
||||
Key.SCREEN_ZOOM -> 100
|
||||
// people wanted a reset on nightly anyways, so this is a good excuse to do so
|
||||
else -> 0
|
||||
}
|
||||
|
||||
private fun defaultValueForStringKey(key: Key) = when (key) {
|
||||
Key.CUSTOM_ASPECT_RATIO -> "16_9"
|
||||
Key.SELECTED_ICON -> "default"
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun keyToName(key: Key): String {
|
||||
return when (key) {
|
||||
Key.LOAD_AUTOMATICALLY -> "PreferenceLoadAutomatically"
|
||||
@@ -179,6 +194,12 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
Key.DEVELOPER_MODE -> "PreferenceDeveloperMode"
|
||||
Key.CLEANUP_APKS -> "PreferenceCleanupPackages"
|
||||
Key.CUSTOM_SYMBOL_LIST -> "PreferenceCustomSymbolList"
|
||||
Key.CUSTOM_ASPECT_RATIO -> "PreferenceCustomAspectRatio"
|
||||
Key.SCREEN_ZOOM -> "PreferenceScreenZoom"
|
||||
Key.SCREEN_ZOOM_FIT -> "PreferenceScreenZoomFit"
|
||||
Key.LIMIT_FRAME_RATE -> "PreferenceLimitFrameRate"
|
||||
Key.DISABLE_USER_THEME -> "PreferenceDisableUserTheme"
|
||||
Key.SELECTED_ICON -> "PreferenceSelectedIcon"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +230,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
|
||||
fun getString(key: Key): String? {
|
||||
val keyName = keyToName(key)
|
||||
return sharedPreferences.getString(keyName, null)
|
||||
return sharedPreferences.getString(keyName, defaultValueForStringKey(key))
|
||||
}
|
||||
|
||||
fun setString(key: Key, value: String?) {
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.geode.launcher.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private const val FILE_KEY = "GeodeLauncherPreferencesFileKey"
|
||||
private const val PROFILES_FILE_KEY = "GeodeLauncherProfiles"
|
||||
|
||||
private const val PROFILE_LIST_SAVE_KEY = "profiles"
|
||||
private const val PROFILE_CURRENT_SAVE_KEY = "currentProfile"
|
||||
|
||||
@Serializable
|
||||
data class Profile(val path: String, val name: String)
|
||||
|
||||
class ProfileManager {
|
||||
private val applicationContext: Context
|
||||
private val preferences: SharedPreferences
|
||||
|
||||
companion object {
|
||||
private lateinit var managerInstance: ProfileManager
|
||||
|
||||
fun get(context: Context): ProfileManager {
|
||||
if (!Companion::managerInstance.isInitialized) {
|
||||
val applicationContext = context.applicationContext
|
||||
|
||||
managerInstance = ProfileManager(applicationContext)
|
||||
}
|
||||
|
||||
return managerInstance
|
||||
}
|
||||
}
|
||||
|
||||
constructor(applicationContext: Context) {
|
||||
this.applicationContext = applicationContext
|
||||
this.preferences = applicationContext.getSharedPreferences(PROFILES_FILE_KEY, Context.MODE_PRIVATE)
|
||||
|
||||
_storedProfiles.value = getProfiles()
|
||||
}
|
||||
|
||||
private val _storedProfiles = MutableStateFlow<List<Profile>>(emptyList())
|
||||
val storedProfiles = _storedProfiles.asStateFlow()
|
||||
|
||||
fun getCurrentProfile(): String? {
|
||||
return preferences.getString(PROFILE_CURRENT_SAVE_KEY, null)
|
||||
.takeUnless { it.isNullOrEmpty() }
|
||||
}
|
||||
|
||||
fun getCurrentFileKey(): String {
|
||||
val currentProfile = getCurrentProfile()
|
||||
?: return FILE_KEY
|
||||
|
||||
return "${FILE_KEY}_${currentProfile}"
|
||||
}
|
||||
|
||||
fun getProfile(path: String): Profile? {
|
||||
val profilesSet = preferences.getStringSet(PROFILE_LIST_SAVE_KEY, null)
|
||||
return profilesSet?.firstNotNullOfOrNull {
|
||||
Json.decodeFromString<Profile>(it).takeIf { it.path == path }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a profile into the save, removing any duplicate profiles beforehand
|
||||
*/
|
||||
fun storeProfile(profile: Profile) {
|
||||
val profilesSet = preferences.getStringSet(PROFILE_LIST_SAVE_KEY, null) ?: emptySet<String>()
|
||||
|
||||
val removedList = profilesSet.filterTo(LinkedHashSet()) {
|
||||
val saveProfile = Json.decodeFromString<Profile>(it)
|
||||
saveProfile.path != profile.path
|
||||
}
|
||||
removedList.add(Json.encodeToString(profile))
|
||||
|
||||
preferences.edit {
|
||||
putStringSet(PROFILE_LIST_SAVE_KEY, removedList)
|
||||
}
|
||||
|
||||
_storedProfiles.value = getProfiles()
|
||||
}
|
||||
|
||||
fun deleteProfile(path: String) {
|
||||
val profilesSet = preferences.getStringSet(PROFILE_LIST_SAVE_KEY, null) ?: emptySet<String>()
|
||||
|
||||
val removedList = profilesSet.filterTo(LinkedHashSet()) {
|
||||
val saveProfile = Json.decodeFromString<Profile>(it)
|
||||
saveProfile.path != path
|
||||
}
|
||||
|
||||
preferences.edit {
|
||||
putStringSet(PROFILE_LIST_SAVE_KEY, removedList)
|
||||
}
|
||||
|
||||
_storedProfiles.value = getProfiles()
|
||||
}
|
||||
|
||||
fun deleteProfiles(paths: List<String>) {
|
||||
val profilesSet = preferences.getStringSet(PROFILE_LIST_SAVE_KEY, null) ?: emptySet<String>()
|
||||
|
||||
val removedList = profilesSet.filterTo(LinkedHashSet()) {
|
||||
val saveProfile = Json.decodeFromString<Profile>(it)
|
||||
!paths.contains(saveProfile.path)
|
||||
}
|
||||
|
||||
preferences.edit(commit = true) {
|
||||
putStringSet(PROFILE_LIST_SAVE_KEY, removedList)
|
||||
}
|
||||
|
||||
_storedProfiles.value = getProfiles()
|
||||
}
|
||||
|
||||
fun setCurrentProfile(profile: String?) {
|
||||
preferences.edit(commit = true) {
|
||||
if (profile.isNullOrEmpty()) {
|
||||
remove(PROFILE_CURRENT_SAVE_KEY)
|
||||
} else {
|
||||
putString(PROFILE_CURRENT_SAVE_KEY, profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getProfiles(): List<Profile> {
|
||||
val profilesSet = preferences.getStringSet(PROFILE_LIST_SAVE_KEY, null) ?: emptySet<String>()
|
||||
|
||||
return profilesSet.map {
|
||||
Json.decodeFromString<Profile>(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.geode.launcher.utils
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2010-2011 cocos2d-x.org
|
||||
|
||||
http://www.cocos2d-x.org
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
package org.cocos2dx.lib
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import android.os.Build
|
||||
import android.view.Surface
|
||||
import android.view.WindowManager
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class Cocos2dxAccelerometer : SensorEventListener {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
external fun onSensorChanged(pX: Float, pY: Float, pZ: Float, pTimestamp: Long)
|
||||
}
|
||||
|
||||
constructor(context: Context) {
|
||||
mContext = WeakReference(context)
|
||||
mSensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
||||
val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
context.display
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
|
||||
}
|
||||
this.mNaturalOrientation = display.rotation
|
||||
}
|
||||
|
||||
private val mContext: WeakReference<Context>
|
||||
private val mSensorManager: SensorManager
|
||||
private val mAccelerometer: Sensor?
|
||||
private val mNaturalOrientation: Int
|
||||
|
||||
fun enable() {
|
||||
this.mSensorManager.registerListener(this, this.mAccelerometer, SensorManager.SENSOR_DELAY_GAME)
|
||||
}
|
||||
|
||||
fun setInterval(interval: Float) {
|
||||
this.mSensorManager.registerListener(this, this.mAccelerometer, (interval * 100000).roundToInt())
|
||||
}
|
||||
|
||||
fun disable() {
|
||||
this.mSensorManager.unregisterListener(this)
|
||||
}
|
||||
|
||||
override fun onSensorChanged(pSensorEvent: SensorEvent) {
|
||||
if (pSensorEvent.sensor.type != Sensor.TYPE_ACCELEROMETER) {
|
||||
return
|
||||
}
|
||||
|
||||
var (x, y, z) = pSensorEvent.values
|
||||
|
||||
/*
|
||||
* Because the axes are not swapped when the device's screen orientation
|
||||
* changes. So we should swap it here. In tablets such as Motorola Xoom,
|
||||
* the default orientation is landscape, so should consider this.
|
||||
*/
|
||||
val orientation = this.mContext.get()!!.resources.configuration.orientation
|
||||
|
||||
if ((orientation == Configuration.ORIENTATION_LANDSCAPE) && (this.mNaturalOrientation != Surface.ROTATION_0)) {
|
||||
val tmp = x
|
||||
x = -y
|
||||
y = tmp
|
||||
} else if ((orientation == Configuration.ORIENTATION_PORTRAIT) && (this.mNaturalOrientation != Surface.ROTATION_0)) {
|
||||
val tmp = x
|
||||
x = y
|
||||
y = -tmp
|
||||
}
|
||||
|
||||
Cocos2dxGLSurfaceView.queueAccelerometer(x, y, z, pSensorEvent.timestamp)
|
||||
}
|
||||
|
||||
override fun onAccuracyChanged(pSensor: Sensor, pAccuracy: Int) {}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package org.cocos2dx.lib
|
||||
|
||||
import android.content.Context
|
||||
import android.view.KeyEvent
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
|
||||
class Cocos2dxEditText(context: Context) : AppCompatEditText(context) {
|
||||
var cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView? = null
|
||||
|
||||
override fun onKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean {
|
||||
super.onKeyDown(keyCode, keyEvent)
|
||||
if (keyCode != KeyEvent.KEYCODE_BACK) {
|
||||
return true
|
||||
}
|
||||
cocos2dxGLSurfaceView?.requestFocus()
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,24 @@
|
||||
package org.cocos2dx.lib
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.hardware.input.InputManager
|
||||
import android.opengl.GLSurfaceView
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Message
|
||||
import android.util.Log
|
||||
import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.Surface
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import com.customRobTop.BaseRobTopActivity
|
||||
import com.geode.launcher.utils.GeodeUtils
|
||||
import kotlin.math.abs
|
||||
|
||||
private const val HANDLER_OPEN_IME_KEYBOARD = 2
|
||||
private const val HANDLER_CLOSE_IME_KEYBOARD = 3
|
||||
@@ -39,6 +44,11 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
msg.what = HANDLER_CLOSE_IME_KEYBOARD
|
||||
handler.sendMessage(msg)
|
||||
}
|
||||
|
||||
fun queueAccelerometer(x: Float, y: Float, z: Float, timestamp: Long) =
|
||||
cocos2dxGLSurfaceView.queueEvent {
|
||||
Cocos2dxAccelerometer.onSensorChanged(x, y, z, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var cocos2dxRenderer: Cocos2dxRenderer
|
||||
@@ -49,22 +59,22 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
}
|
||||
|
||||
var sendTimestampEvents = false
|
||||
var manualBackEvents = false
|
||||
var controllerEventsEnabled = false
|
||||
private set
|
||||
|
||||
var cocos2dxEditText: Cocos2dxEditText? = null
|
||||
var cocos2dxEditText: AppCompatEditText? = null
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
field?.setOnEditorActionListener(cocos2dxTextInputWrapper)
|
||||
field?.cocos2dxGLSurfaceView = this
|
||||
requestFocus()
|
||||
}
|
||||
|
||||
fun initView() {
|
||||
setEGLContextClientVersion(2)
|
||||
isFocusableInTouchMode = true
|
||||
// if (Build.VERSION.SDK_INT >= 11) {
|
||||
preserveEGLContextOnPause = true
|
||||
// }
|
||||
cocos2dxGLSurfaceView = this
|
||||
cocos2dxTextInputWrapper = Cocos2dxTextInputWrapper(this)
|
||||
Cocos2dxGLSurfaceView.handler = object : Handler(Looper.getMainLooper()) {
|
||||
@@ -117,23 +127,39 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
fun updateRefreshRate() {
|
||||
val maxRefreshRate = display.supportedModes.maxBy { it.refreshRate }
|
||||
holder.surface.setFrameRate(
|
||||
maxRefreshRate.refreshRate,
|
||||
Surface.FRAME_RATE_COMPATIBILITY_DEFAULT
|
||||
)
|
||||
val chosenDisplay = display.supportedModes?.maxByOrNull { it.refreshRate }
|
||||
if (chosenDisplay == null) {
|
||||
println("updateRefreshRate failed to find a display to maximize refresh rate...")
|
||||
return
|
||||
}
|
||||
|
||||
val chosenRefreshRate = chosenDisplay.refreshRate
|
||||
|
||||
println("updateRefreshRate: selecting refresh rate of ${chosenDisplay.refreshRate} (display ${chosenDisplay.modeId})")
|
||||
|
||||
holder.surface.setFrameRate(chosenRefreshRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT)
|
||||
if (isAttachedToWindow) {
|
||||
(context as Activity).window.attributes.preferredRefreshRate = chosenRefreshRate
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
queueEvent { cocos2dxRenderer.handleOnPause() }
|
||||
renderMode = RENDERMODE_WHEN_DIRTY
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
renderMode = RENDERMODE_CONTINUOUSLY
|
||||
queueEvent { cocos2dxRenderer.handleOnResume() }
|
||||
}
|
||||
|
||||
private fun sendNextEventTimestamp(timestamp: Long) {
|
||||
if (sendTimestampEvents)
|
||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
||||
}
|
||||
|
||||
override fun onTouchEvent(motionEvent: MotionEvent): Boolean {
|
||||
val pointerNumber = motionEvent.pointerCount
|
||||
val ids = IntArray(pointerNumber)
|
||||
@@ -153,10 +179,16 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
val xDown = xs[0]
|
||||
val f = ys[0]
|
||||
queueEvent {
|
||||
if (sendTimestampEvents)
|
||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
||||
sendNextEventTimestamp(timestamp)
|
||||
|
||||
cocos2dxRenderer.handleActionDown(idDown, xDown, f)
|
||||
if (controllerEventsEnabled)
|
||||
GeodeUtils.internalTouchEvent(
|
||||
timestamp, motionEvent.deviceId, motionEvent.source,
|
||||
// slice here to convert to single element array
|
||||
GeodeUtils.TOUCH_TYPE_BEGIN, ids.sliceArray(0..0), xs.sliceArray(0..0), ys.sliceArray(0..0)
|
||||
)
|
||||
else
|
||||
cocos2dxRenderer.handleActionDown(idDown, xDown, f)
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -165,28 +197,43 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
val f2 = xs[0]
|
||||
val f3 = ys[0]
|
||||
queueEvent {
|
||||
if (sendTimestampEvents)
|
||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
||||
sendNextEventTimestamp(timestamp)
|
||||
|
||||
cocos2dxRenderer.handleActionUp(idUp, f2, f3)
|
||||
if (controllerEventsEnabled)
|
||||
GeodeUtils.internalTouchEvent(
|
||||
timestamp, motionEvent.deviceId, motionEvent.source,
|
||||
GeodeUtils.TOUCH_TYPE_END, ids.sliceArray(0..0), xs.sliceArray(0..0), ys.sliceArray(0..0)
|
||||
)
|
||||
else
|
||||
cocos2dxRenderer.handleActionUp(idUp, f2, f3)
|
||||
}
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
queueEvent {
|
||||
if (sendTimestampEvents)
|
||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
||||
sendNextEventTimestamp(timestamp)
|
||||
|
||||
cocos2dxRenderer.handleActionMove(ids, xs, ys)
|
||||
if (controllerEventsEnabled)
|
||||
GeodeUtils.internalTouchEvent(
|
||||
timestamp, motionEvent.deviceId, motionEvent.source,
|
||||
GeodeUtils.TOUCH_TYPE_MOVE, ids, xs, ys
|
||||
)
|
||||
else
|
||||
cocos2dxRenderer.handleActionMove(ids, xs, ys)
|
||||
}
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
queueEvent {
|
||||
if (sendTimestampEvents)
|
||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
||||
sendNextEventTimestamp(timestamp)
|
||||
|
||||
cocos2dxRenderer.handleActionCancel(ids, xs, ys)
|
||||
if (controllerEventsEnabled)
|
||||
GeodeUtils.internalTouchEvent(
|
||||
timestamp, motionEvent.deviceId, motionEvent.source,
|
||||
GeodeUtils.TOUCH_TYPE_CANCEL, ids, xs, ys
|
||||
)
|
||||
else
|
||||
cocos2dxRenderer.handleActionCancel(ids, xs, ys)
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -196,10 +243,15 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
val xPointerDown = motionEvent.getX(indexPointerDown)
|
||||
val y = motionEvent.getY(indexPointerDown)
|
||||
queueEvent {
|
||||
if (sendTimestampEvents)
|
||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
||||
sendNextEventTimestamp(timestamp)
|
||||
|
||||
cocos2dxRenderer.handleActionDown(idPointerDown, xPointerDown, y)
|
||||
if (controllerEventsEnabled)
|
||||
GeodeUtils.internalTouchEvent(
|
||||
timestamp, motionEvent.deviceId, motionEvent.source,
|
||||
GeodeUtils.TOUCH_TYPE_BEGIN, intArrayOf(idPointerDown), floatArrayOf(xPointerDown), floatArrayOf(y)
|
||||
)
|
||||
else
|
||||
cocos2dxRenderer.handleActionDown(idPointerDown, xPointerDown, y)
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -209,10 +261,15 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
val xPointerUp = motionEvent.getX(indexPointUp)
|
||||
val y2 = motionEvent.getY(indexPointUp)
|
||||
queueEvent {
|
||||
if (sendTimestampEvents)
|
||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
||||
sendNextEventTimestamp(timestamp)
|
||||
|
||||
cocos2dxRenderer.handleActionUp(idPointerUp, xPointerUp, y2)
|
||||
if (controllerEventsEnabled)
|
||||
GeodeUtils.internalTouchEvent(
|
||||
timestamp, motionEvent.deviceId, motionEvent.source,
|
||||
GeodeUtils.TOUCH_TYPE_END, intArrayOf(idPointerUp), floatArrayOf(xPointerUp), floatArrayOf(y2)
|
||||
)
|
||||
else
|
||||
cocos2dxRenderer.handleActionUp(idPointerUp, xPointerUp, y2)
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -244,8 +301,51 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun sendKeyBack() {
|
||||
if (cocos2dxEditText?.isFocused == true) {
|
||||
requestFocus()
|
||||
}
|
||||
|
||||
if (BaseRobTopActivity.blockBackButton) {
|
||||
return
|
||||
}
|
||||
|
||||
val currentTime = System.nanoTime()
|
||||
queueEvent {
|
||||
sendNextEventTimestamp(currentTime)
|
||||
|
||||
if (controllerEventsEnabled) {
|
||||
GeodeUtils.internalKeyEvent(
|
||||
currentTime,
|
||||
-1,
|
||||
InputDevice.SOURCE_ANY,
|
||||
KeyEvent.KEYCODE_BACK,
|
||||
0,
|
||||
true,
|
||||
0
|
||||
)
|
||||
GeodeUtils.internalKeyEvent(
|
||||
currentTime,
|
||||
-1,
|
||||
InputDevice.SOURCE_ANY,
|
||||
KeyEvent.KEYCODE_BACK,
|
||||
0,
|
||||
false,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
if (useKeyboardEvents) {
|
||||
GeodeUtils.nativeKeyDown(KeyEvent.KEYCODE_BACK, 0, false)
|
||||
GeodeUtils.nativeKeyUp(KeyEvent.KEYCODE_BACK, 0)
|
||||
} else if (!controllerEventsEnabled) {
|
||||
cocos2dxRenderer.handleKeyDown(KeyEvent.KEYCODE_BACK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||
if (!useKeyboardEvents) {
|
||||
if (!useKeyboardEvents && !controllerEventsEnabled) {
|
||||
return legacyKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
@@ -260,10 +360,15 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
}
|
||||
|
||||
queueEvent {
|
||||
if (sendTimestampEvents)
|
||||
GeodeUtils.setNextInputTimestamp(event.eventTime * MS_TO_NS)
|
||||
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
|
||||
|
||||
GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0)
|
||||
if (useKeyboardEvents) {
|
||||
GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0)
|
||||
}
|
||||
|
||||
if (controllerEventsEnabled) {
|
||||
GeodeUtils.internalKeyEvent(event.eventTime * MS_TO_NS, event.deviceId, event.source, event.keyCode, event.modifiers, true, event.repeatCount)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -271,7 +376,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
}
|
||||
|
||||
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
|
||||
if (!useKeyboardEvents) {
|
||||
if (!useKeyboardEvents && !controllerEventsEnabled) {
|
||||
return super.onKeyUp(keyCode, event)
|
||||
}
|
||||
|
||||
@@ -285,10 +390,15 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
}
|
||||
|
||||
queueEvent {
|
||||
if (sendTimestampEvents)
|
||||
GeodeUtils.setNextInputTimestamp(event.eventTime * MS_TO_NS)
|
||||
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
|
||||
|
||||
GeodeUtils.nativeKeyUp(keyCode, event.modifiers)
|
||||
if (useKeyboardEvents) {
|
||||
GeodeUtils.nativeKeyUp(keyCode, event.modifiers)
|
||||
}
|
||||
|
||||
if (controllerEventsEnabled) {
|
||||
GeodeUtils.internalKeyEvent(event.eventTime * MS_TO_NS, event.deviceId, event.source, event.keyCode, event.modifiers, false, event.repeatCount)
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
@@ -296,8 +406,67 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
}
|
||||
}
|
||||
|
||||
// i'm lazy.
|
||||
// https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input
|
||||
private fun getCenteredAxis(
|
||||
event: MotionEvent,
|
||||
device: InputDevice,
|
||||
axis: Int,
|
||||
historyPos: Int
|
||||
): Float {
|
||||
device.getMotionRange(axis, event.source)?.apply {
|
||||
val value: Float = if (historyPos < 0) {
|
||||
event.getAxisValue(axis)
|
||||
} else {
|
||||
event.getHistoricalAxisValue(axis, historyPos)
|
||||
}
|
||||
|
||||
if (abs(value) > flat) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return 0.0f
|
||||
}
|
||||
|
||||
private fun getJoystickMotion(event: MotionEvent, axisHorizontal: Int, axisVertical: Int): Pair<FloatArray, FloatArray> {
|
||||
val positionChangesX = ArrayList<Float>()
|
||||
val positionChangesY = ArrayList<Float>()
|
||||
|
||||
val inputDevice = event.device
|
||||
|
||||
(0 until event.historySize).forEach { i ->
|
||||
val x = getCenteredAxis(event, inputDevice, axisHorizontal, i)
|
||||
val y = getCenteredAxis(event, inputDevice, axisVertical, i)
|
||||
positionChangesX.add(x)
|
||||
positionChangesY.add(y)
|
||||
}
|
||||
|
||||
val x = getCenteredAxis(event, inputDevice, axisHorizontal, -1)
|
||||
val y = getCenteredAxis(event, inputDevice, axisVertical, -1)
|
||||
positionChangesX.add(x)
|
||||
positionChangesY.add(y)
|
||||
|
||||
return Pair(positionChangesX.toFloatArray(), positionChangesY.toFloatArray())
|
||||
}
|
||||
|
||||
private fun getJoystickMotion(event: MotionEvent, axis: Int): FloatArray {
|
||||
val positionChanges = ArrayList<Float>()
|
||||
|
||||
val inputDevice = event.device
|
||||
|
||||
(0 until event.historySize).forEach { i ->
|
||||
val x = getCenteredAxis(event, inputDevice, axis, i)
|
||||
positionChanges.add(x)
|
||||
}
|
||||
|
||||
val x = getCenteredAxis(event, inputDevice, axis, -1)
|
||||
positionChanges.add(x)
|
||||
|
||||
return positionChanges.toFloatArray()
|
||||
}
|
||||
|
||||
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
|
||||
if (!useKeyboardEvents) {
|
||||
if (!useKeyboardEvents && !controllerEventsEnabled) {
|
||||
return super.onGenericMotionEvent(event)
|
||||
}
|
||||
|
||||
@@ -308,10 +477,40 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
event.eventTimeNanos else event.eventTime * MS_TO_NS
|
||||
|
||||
queueEvent {
|
||||
if (sendTimestampEvents)
|
||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
||||
sendNextEventTimestamp(timestamp)
|
||||
|
||||
GeodeUtils.nativeActionScroll(scrollX, scrollY)
|
||||
if (useKeyboardEvents) {
|
||||
GeodeUtils.nativeActionScroll(scrollX, scrollY)
|
||||
}
|
||||
|
||||
if (controllerEventsEnabled) {
|
||||
GeodeUtils.internalScrollEvent(timestamp, event.deviceId, event.source, scrollX, scrollY)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if (controllerEventsEnabled && event?.action == MotionEvent.ACTION_MOVE && event.source and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK) {
|
||||
val (motionLeftX, motionLeftY) = getJoystickMotion(event, MotionEvent.AXIS_X, MotionEvent.AXIS_Y)
|
||||
val (motionRightX, motionRightY) = getJoystickMotion(event, MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ)
|
||||
val (motionDPadX, motionDPadY) = getJoystickMotion(event, MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y)
|
||||
|
||||
val motionLTrigger = getJoystickMotion(event, MotionEvent.AXIS_LTRIGGER)
|
||||
val motionRTrigger = getJoystickMotion(event, MotionEvent.AXIS_RTRIGGER)
|
||||
|
||||
// i'm sure this is great for performance
|
||||
val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
event.eventTimeNanos else event.eventTime * MS_TO_NS
|
||||
|
||||
queueEvent {
|
||||
GeodeUtils.internalJoystickEvent(
|
||||
timestamp, event.deviceId, event.source,
|
||||
motionLeftX, motionLeftY,
|
||||
motionRightX, motionRightY,
|
||||
motionDPadX, motionDPadY,
|
||||
motionLTrigger, motionRTrigger
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -336,4 +535,40 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
fun deleteBackward() {
|
||||
queueEvent { cocos2dxRenderer.handleDeleteBackward() }
|
||||
}
|
||||
|
||||
fun enableControllerEvents() {
|
||||
if (controllerEventsEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
controllerEventsEnabled = true
|
||||
|
||||
val inputManager = context.getSystemService(Context.INPUT_SERVICE) as InputManager
|
||||
inputManager.registerInputDeviceListener(InputListener, null)
|
||||
}
|
||||
|
||||
fun disableControllerEvents() {
|
||||
if (!controllerEventsEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
controllerEventsEnabled = true
|
||||
|
||||
val inputManager = context.getSystemService(Context.INPUT_SERVICE) as InputManager
|
||||
inputManager.unregisterInputDeviceListener(InputListener)
|
||||
}
|
||||
|
||||
object InputListener : InputManager.InputDeviceListener {
|
||||
override fun onInputDeviceAdded(deviceId: Int) {
|
||||
GeodeUtils.inputDeviceAdded(deviceId)
|
||||
}
|
||||
|
||||
override fun onInputDeviceChanged(deviceId: Int) {
|
||||
GeodeUtils.inputDeviceChanged(deviceId)
|
||||
}
|
||||
|
||||
override fun onInputDeviceRemoved(deviceId: Int) {
|
||||
GeodeUtils.inputDeviceRemoved(deviceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package org.cocos2dx.lib
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.res.AssetManager
|
||||
import android.os.Process
|
||||
import com.customRobTop.BaseRobTopActivity
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
@@ -11,9 +12,9 @@ import com.geode.launcher.utils.LaunchUtils
|
||||
object Cocos2dxHelper {
|
||||
// private val sCocos2dMusic: Cocos2dxMusic? = null
|
||||
// private val sCocos2dSound: Cocos2dxSound? = null
|
||||
// private val sAssetManager: AssetManager? = null
|
||||
// private val sCocos2dxAccelerometer: Cocos2dxAccelerometer? = null
|
||||
// private val sAccelerometerEnabled = false
|
||||
private var sAssetManager: AssetManager? = null
|
||||
private var sCocos2dxAccelerometer: Cocos2dxAccelerometer? = null
|
||||
private var sAccelerometerEnabled = false
|
||||
private var packageName: String? = null
|
||||
private var fileDirectory: String? = null
|
||||
|
||||
@@ -36,6 +37,40 @@ object Cocos2dxHelper {
|
||||
Process.killProcess(Process.myPid())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getAssetManager(): AssetManager? {
|
||||
return sAssetManager
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun enableAccelerometer() {
|
||||
sAccelerometerEnabled = true
|
||||
sCocos2dxAccelerometer?.enable()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setAccelerometerInterval(interval: Float) {
|
||||
sCocos2dxAccelerometer?.setInterval(interval)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun disableAccelerometer() {
|
||||
sAccelerometerEnabled = false
|
||||
sCocos2dxAccelerometer?.disable()
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
if (sAccelerometerEnabled) {
|
||||
sCocos2dxAccelerometer?.enable()
|
||||
}
|
||||
}
|
||||
|
||||
fun onPause() {
|
||||
if (sAccelerometerEnabled) {
|
||||
sCocos2dxAccelerometer?.disable()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDPI(): Int {
|
||||
return BaseRobTopActivity.me.get()?.resources?.configuration?.densityDpi ?: -1
|
||||
@@ -52,14 +87,14 @@ object Cocos2dxHelper {
|
||||
this.cocos2dxHelperListener = cocos2dxHelperListener
|
||||
packageName = applicationInfo.packageName
|
||||
fileDirectory = LaunchUtils.getSaveDirectory(context).absolutePath
|
||||
// Cocos2dxHelper.sCocos2dxAccelerometer = Cocos2dxAccelerometer(pContext)
|
||||
sCocos2dxAccelerometer = Cocos2dxAccelerometer(context)
|
||||
// Cocos2dxHelper.sCocos2dMusic = Cocos2dxMusic(pContext)
|
||||
// var simultaneousStreams: Int = Cocos2dxSound.MAX_SIMULTANEOUS_STREAMS_DEFAULT
|
||||
// if (Cocos2dxHelper.getDeviceModel().indexOf("GT-I9100") !== -1) {
|
||||
// simultaneousStreams = Cocos2dxSound.MAX_SIMULTANEOUS_STREAMS_I9100
|
||||
// }
|
||||
// Cocos2dxHelper.sCocos2dSound = Cocos2dxSound(pContext, simultaneousStreams)
|
||||
// Cocos2dxHelper.sAssetManager = pContext.getAssets()
|
||||
sAssetManager = context.assets
|
||||
Cocos2dxBitmap.setContext(context)
|
||||
// Cocos2dxETCLoader.setContext(pContext)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import android.os.Build
|
||||
import javax.microedition.khronos.egl.EGLConfig
|
||||
import javax.microedition.khronos.opengles.GL10
|
||||
|
||||
private const val NANOSECONDS_PER_SECOND = 1000000000L
|
||||
private const val NANOSECONDS_PER_MICROSECOND = 1000000L
|
||||
|
||||
@Suppress("unused", "KotlinJniMissingFunction")
|
||||
class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceView.Renderer {
|
||||
companion object {
|
||||
@@ -62,6 +65,11 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi
|
||||
private var screenHeight = 0
|
||||
var sendResizeEvents = false
|
||||
var setFrameRate = false
|
||||
private var mAnimationInterval: Long? = null
|
||||
|
||||
fun limitFrameRate(rate: Int) {
|
||||
mAnimationInterval = NANOSECONDS_PER_SECOND/rate
|
||||
}
|
||||
|
||||
fun setScreenWidthAndHeight(surfaceWidth: Int, surfaceHeight: Int) {
|
||||
screenWidth = surfaceWidth
|
||||
@@ -77,10 +85,28 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi
|
||||
if (setFrameRate && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
handler.updateRefreshRate()
|
||||
}
|
||||
|
||||
println("renderer surfaceChanged: ${width}x${height}@${handler.display.refreshRate}fps")
|
||||
}
|
||||
|
||||
override fun onDrawFrame(gl: GL10?) {
|
||||
nativeRender()
|
||||
val animationInterval = mAnimationInterval
|
||||
if (animationInterval == null) {
|
||||
nativeRender()
|
||||
} else {
|
||||
nativeRender()
|
||||
|
||||
val now = System.nanoTime()
|
||||
val interval = now - this.lastTickInNanoSeconds
|
||||
|
||||
if (interval < animationInterval) {
|
||||
try {
|
||||
Thread.sleep((animationInterval - interval) / NANOSECONDS_PER_MICROSECOND)
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
|
||||
lastTickInNanoSeconds = System.nanoTime()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleActionDown(id: Int, x: Float, y: Float) {
|
||||
|
||||
@@ -52,12 +52,10 @@ class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurf
|
||||
cocos2dxGLSurfaceView.deleteBackward()
|
||||
}
|
||||
var text = textView.text.toString()
|
||||
if (text.compareTo("") == 0) {
|
||||
text = "\n"
|
||||
}
|
||||
if (10 != text[text.length - 1].code) {
|
||||
text += 10.toChar()
|
||||
if (!text.endsWith('\n')) {
|
||||
text += '\n'
|
||||
}
|
||||
|
||||
cocos2dxGLSurfaceView.insertText(text)
|
||||
}
|
||||
if (actionID != 6) {
|
||||
|
||||
@@ -10,8 +10,9 @@ object Cocos2dxTypefaces {
|
||||
|
||||
@Synchronized
|
||||
operator fun get(pContext: Context, pAssetName: String): Typeface? {
|
||||
var typeface: Typeface?
|
||||
var typeface2: Typeface
|
||||
val typeface: Typeface?
|
||||
val typeface2: Typeface
|
||||
|
||||
synchronized(Cocos2dxTypefaces::class.java) {
|
||||
if (!typefaceCache.containsKey(pAssetName)) {
|
||||
typeface2 = if (pAssetName.startsWith("/")) {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<group android:scaleX="0.6689189"
|
||||
android:scaleY="0.6689189"
|
||||
android:translateX="169.51352"
|
||||
android:translateY="182.1982">
|
||||
<path
|
||||
android:pathData="M553.7,189.9L536.9,177.1C522.2,165.8 501.8,165.8 487.1,177.1L207.6,390.5C194,400.9 188.3,418.8 193.3,435.2L300.9,783.2C306.2,800.3 322.1,812.1 340.1,812.1H683.9C701.9,812.1 717.8,800.3 723.1,783.2L830.7,435.2C835.7,418.8 830,400.9 816.4,390.5L792,371.9L762,466.3L680,733C676.5,744.4 666,752.1 654.2,752.1H368C356.1,752.1 345.7,744.4 342.2,733L252.8,442.3C249.5,431.5 253.2,419.8 262.1,413L494.6,234.5C495.6,233.7 496.6,233.1 497.6,232.5L553.7,189.9Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.25"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M500.6,320.6C508.5,314.5 519.5,314.5 527.4,320.6L686,442.2C693.3,447.8 696.3,457.3 693.6,466.1L632.6,664.4C629.7,673.6 621.2,679.9 611.6,679.9H416.4C406.8,679.9 398.3,673.6 395.4,664.4L385.4,632H439.4C440,632.1 440.7,632.2 441.4,632.2H583.2C588,632.2 592.3,629 593.7,624.4L638,477.5C639.3,473.2 637.8,468.5 634.3,465.7L519.1,375.6C515.1,372.5 509.5,372.5 505.5,375.6L399.1,458.8L344.5,499.1L334.4,466.1C331.7,457.3 334.7,447.8 342,442.2L500.6,320.6Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.25"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M557.1,165.4L536.9,150C522.2,138.8 501.8,138.8 487.1,150L180.9,383.8C167.3,394.2 161.6,412.1 166.7,428.5L284.4,809.4C289.7,826.6 305.6,838.3 323.6,838.3H700.4C718.4,838.3 734.3,826.6 739.6,809.4L857.3,428.5C862.4,412.1 856.7,394.2 843.1,383.8L814.8,362.3L782.4,464.4L693.2,754.4C689.7,765.7 679.2,773.5 667.4,773.5H354.6C342.8,773.5 332.3,765.7 328.8,754.4L231.2,437C227.9,426.2 231.6,414.5 240.6,407.6L494.6,212.6C495.6,211.8 496.6,211.1 497.7,210.5L557.1,165.4Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#ffffff" />
|
||||
<path
|
||||
android:pathData="M500.6,284.3C508.5,278.2 519.5,278.2 527.4,284.3L716.8,429.5C724.1,435.1 727.1,444.6 724.4,453.4L651.6,689.9C648.8,699.2 640.3,705.5 630.6,705.5H397.4C387.7,705.5 379.2,699.2 376.4,689.9L363.9,649.5H425.7C426.4,649.6 427.1,649.7 427.9,649.7H596.1C601,649.7 605.2,646.5 606.6,641.9L659.1,468C660.5,463.6 659,458.9 655.4,456.1L518.8,349.3C514.8,346.2 509.2,346.2 505.2,349.3L379.9,447.3L316.2,494.3L303.6,453.4C300.9,444.6 303.9,435.1 311.2,429.5L500.6,284.3Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#ffffff" />
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,28 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
|
||||
<clip-path
|
||||
android:pathData="M0,0h1024v1024h-1024z"/>
|
||||
<path
|
||||
android:pathData="M0,0h1024v1024h-1024z"
|
||||
android:fillColor="#F4D48E"/>
|
||||
<path
|
||||
android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
|
||||
android:fillColor="#F4D48E"/>
|
||||
<path
|
||||
android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
|
||||
android:fillColor="#F4A47B"/>
|
||||
<path
|
||||
android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
|
||||
android:fillColor="#E27880"/>
|
||||
<path
|
||||
android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
|
||||
android:fillColor="#B9588F"/>
|
||||
<path
|
||||
android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
|
||||
android:fillColor="#714A9A"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,28 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
|
||||
<clip-path
|
||||
android:pathData="M0,0h1024v1024h-1024z"/>
|
||||
<path
|
||||
android:pathData="M0,0h1024v1024h-1024z"
|
||||
android:fillColor="#CC7C7C"/>
|
||||
<path
|
||||
android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
|
||||
android:fillColor="#CC7C7C"/>
|
||||
<path
|
||||
android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
|
||||
android:fillColor="#C6B471"/>
|
||||
<path
|
||||
android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
|
||||
android:fillColor="#59AF7F"/>
|
||||
<path
|
||||
android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
|
||||
android:fillColor="#5F8CAB"/>
|
||||
<path
|
||||
android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
|
||||
android:fillColor="#70558D"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,28 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
|
||||
<clip-path
|
||||
android:pathData="M0,0h1024v1024h-1024z"/>
|
||||
<path
|
||||
android:pathData="M0,0h1024v1024h-1024z"
|
||||
android:fillColor="#65B8C2"/>
|
||||
<path
|
||||
android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
|
||||
android:fillColor="#65B8C2"/>
|
||||
<path
|
||||
android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
|
||||
android:fillColor="#DEA5CD"/>
|
||||
<path
|
||||
android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
|
||||
android:fillColor="#DEE8EA"/>
|
||||
<path
|
||||
android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
|
||||
android:fillColor="#C485BD"/>
|
||||
<path
|
||||
android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
|
||||
android:fillColor="#259CB4"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L400,160L480,240L800,240Q833,240 856.5,263.5Q880,287 880,320L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720L800,720Q800,720 800,720Q800,720 800,720L800,320Q800,320 800,320Q800,320 800,320L447,320L367,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720ZM160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240L160,320L160,320Q160,320 160,320Q160,320 160,320L160,720Q160,720 160,720Q160,720 160,720Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M440,680L280,680Q197,680 138.5,621.5Q80,563 80,480Q80,397 138.5,338.5Q197,280 280,280L440,280L440,360L280,360Q230,360 195,395Q160,430 160,480Q160,530 195,565Q230,600 280,600L440,600L440,680ZM320,520L320,440L640,440L640,520L320,520ZM520,680L520,600L680,600Q730,600 765,565Q800,530 800,480Q800,430 765,395Q730,360 680,360L520,360L520,280L680,280Q763,280 821.5,338.5Q880,397 880,480Q880,563 821.5,621.5Q763,680 680,680L520,680Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,480Q414,480 367,433Q320,386 320,320Q320,254 367,207Q414,160 480,160Q546,160 593,207Q640,254 640,320Q640,386 593,433Q546,480 480,480ZM160,800L160,688Q160,654 177.5,625.5Q195,597 224,582Q286,551 350,535.5Q414,520 480,520Q546,520 610,535.5Q674,551 736,582Q765,597 782.5,625.5Q800,654 800,688L800,800L160,800ZM240,720L720,720L720,688Q720,677 714.5,668Q709,659 700,654Q646,627 591,613.5Q536,600 480,600Q424,600 369,613.5Q314,627 260,654Q251,659 245.5,668Q240,677 240,688L240,720ZM480,400Q513,400 536.5,376.5Q560,353 560,320Q560,287 536.5,263.5Q513,240 480,240Q447,240 423.5,263.5Q400,287 400,320Q400,353 423.5,376.5Q447,400 480,400ZM480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320ZM480,720L480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720L480,720Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M720,560L720,440L600,440L600,360L720,360L720,240L800,240L800,360L920,360L920,440L800,440L800,560L720,560ZM360,480Q294,480 247,433Q200,386 200,320Q200,254 247,207Q294,160 360,160Q426,160 473,207Q520,254 520,320Q520,386 473,433Q426,480 360,480ZM40,800L40,688Q40,654 57.5,625.5Q75,597 104,582Q166,551 230,535.5Q294,520 360,520Q426,520 490,535.5Q554,551 616,582Q645,597 662.5,625.5Q680,654 680,688L680,800L40,800ZM120,720L600,720L600,688Q600,677 594.5,668Q589,659 580,654Q526,627 471,613.5Q416,600 360,600Q304,600 249,613.5Q194,627 140,654Q131,659 125.5,668Q120,677 120,688L120,720ZM360,400Q393,400 416.5,376.5Q440,353 440,320Q440,287 416.5,263.5Q393,240 360,240Q327,240 303.5,263.5Q280,287 280,320Q280,353 303.5,376.5Q327,400 360,400ZM360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320ZM360,720L360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720L360,720Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M200,520L200,440L760,440L760,520L200,520Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M280,760L280,680L564,680Q627,680 673.5,640Q720,600 720,540Q720,480 673.5,440Q627,400 564,400L312,400L416,504L360,560L160,360L360,160L416,216L312,320L564,320Q661,320 730.5,383Q800,446 800,540Q800,634 730.5,697Q661,760 564,760L280,760Z"/>
|
||||
</vector>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_geode_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_monochrome_shadow"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_pride_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_monochrome_shadow"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_trans_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_monochrome_shadow"/>
|
||||
</adaptive-icon>
|
||||
@@ -3,7 +3,7 @@
|
||||
<string name="no_activity_found">No activity found for this action.</string>
|
||||
|
||||
<!-- MainActivity strings -->
|
||||
<string name="game_not_found">Geometry Dash was not found.</string>
|
||||
<string name="game_not_found">Geometry Dash was not found.\nThe full version of the game is required.</string>
|
||||
<string name="launcher_launch">Launch</string>
|
||||
<string name="launcher_settings">Settings</string>
|
||||
<string name="launcher_download">Download</string>
|
||||
@@ -12,7 +12,7 @@
|
||||
<string name="launcher_logo_alt">geode logo</string>
|
||||
<string name="launcher_launch_icon_alt">play icon</string>
|
||||
<string name="launcher_download_icon_alt">download icon</string>
|
||||
<string name="launcher_download_game">Download Game</string>
|
||||
<string name="launcher_download_game">Download game</string>
|
||||
<string name="launcher_download_32bit">Download the 32-bit version of launcher.</string>
|
||||
<string name="launcher_download_universal">Download the non 32-bit version of launcher.</string>
|
||||
<string name="launcher_settings_icon_alt">settings icon</string>
|
||||
@@ -33,8 +33,17 @@
|
||||
<string name="launcher_unsupported_version_description">
|
||||
The current version of Geometry Dash is not compatible with the version supported by the launcher. A launcher update is required to fix this.\nLaunch will continue, but crashes may occur!
|
||||
</string>
|
||||
|
||||
<string name="launcher_game_update_required_short">A Geometry Dash update is required.</string>
|
||||
<string name="launcher_game_update_required">The currently installed version of Geometry Dash is no longer supported. Please update Geometry Dash to continue receiving Geode updates.</string>
|
||||
|
||||
<string name="launcher_update_available">A new launcher update is available!</string>
|
||||
<string name="launcher_update_dismiss">Dismiss</string>
|
||||
<string name="launcher_update_view">View</string>
|
||||
<string name="launcher_update_external_link">View in browser</string>
|
||||
<string name="launcher_update_name">Geode Launcher v%1$s</string>
|
||||
<string name="launcher_update_changelog">Changelog</string>
|
||||
<string name="launcher_update_released">Released %1$s</string>
|
||||
<string name="safe_mode_enable_title">Enable safe mode?</string>
|
||||
<string name="safe_mode_enable_description">Safe mode disables loading of all mods, allowing one to remove problematic mods or diagnose crashes. If you\'re not experiencing issues with Geode, you should probably not be using safe mode.</string>
|
||||
<string name="launcher_failed_to_load">Geode failed to load.</string>
|
||||
@@ -46,17 +55,19 @@
|
||||
<string name="launcher_error_copy">Copy details</string>
|
||||
<string name="launcher_error_export_crash">Share crash</string>
|
||||
<string name="launcher_error_export_missing">No crash dump was found!</string>
|
||||
<string name="launcher_error_device_info">Android %2$s, %1$s\nGeometry Dash %4$s with Geode %3$s</string>
|
||||
<string name="launcher_error_device_info">Android %2$s, %1$s (%5$s)\nGD %4$s, Geode %3$s + v%6$s</string>
|
||||
|
||||
<string name="launcher_notification_settings">Launcher Settings</string>
|
||||
<string name="launcher_notification_update_failed">Failed to check for updates.</string>
|
||||
<string name="launcher_notification_update_failed">Failed to update Geode.</string>
|
||||
<string name="launcher_notification_update_success">Geode updated!</string>
|
||||
<string name="launcher_notification_update_cta">Tap to install.</string>
|
||||
<string name="launcher_notification_compatibility">Unsupported Geometry Dash version.</string>
|
||||
<string name="launcher_notification_compatibility_description">An update is required to receive Geode updates.</string>
|
||||
<string name="launcher_notification_update_cta">Tap for more information.</string>
|
||||
|
||||
<string name="launcher_starting_game">Starting Geometry Dash…</string>
|
||||
<string name="launcher_downloading_update">Downloading update…</string>
|
||||
<string name="launcher_downloading_update_details">(%1$s/%2$s)</string>
|
||||
<string name="launcher_cancelled_manual">Launch cancelled.</string>
|
||||
<string name="launcher_cancelled_manual">Launch paused.</string>
|
||||
<string name="launcher_cancelled_navigated_away">Launch paused.</string>
|
||||
<string name="launcher_cancelled_crash">Launch paused due to a crash.</string>
|
||||
<string name="launcher_cancelled_error">Could not start Geode.</string>
|
||||
@@ -73,6 +84,7 @@
|
||||
<string name="release_fetch_in_progress">Checking for new releases…</string>
|
||||
<string name="release_fetch_success">Geode updated!</string>
|
||||
<string name="release_fetch_downloading">Downloading update… (%1$s/%2$s)</string>
|
||||
<string name="release_fetch_downloading_indeterminate">Downloading update… (%1$s)</string>
|
||||
<string name="release_fetch_cancelling">Cancelling update…</string>
|
||||
<string name="release_fetch_no_internet">No internet found.</string>
|
||||
<string name="release_fetch_cancelled">Update cancelled.</string>
|
||||
@@ -95,6 +107,7 @@
|
||||
<string name="load_failed_recommendation_update">Verify that the latest versions of the launcher and Geometry Dash are installed.</string>
|
||||
<string name="load_failed_recommendation_report">Report this issue to the developers.</string>
|
||||
<string name="load_failed_recommendation_safe_mode">Hold the Launch button to launch in safe mode.</string>
|
||||
<string name="load_failed_recommendation_dev_stl">Devs: If this is a locally built Geode version, please use the c++_shared ANDROID_STL!</string>
|
||||
|
||||
<string name="message_box_cancel">Cancel</string>
|
||||
<string name="message_box_accept">OK</string>
|
||||
@@ -107,7 +120,7 @@
|
||||
<string name="title_activity_settings">Settings</string>
|
||||
<string name="back_icon_alt">back</string>
|
||||
|
||||
<string name="export_folder_tag">Launcher External Folder</string>
|
||||
<string name="export_folder_tag">Launcher external folder</string>
|
||||
<string name="text_copied">Copied to clipboard.</string>
|
||||
|
||||
<string name="preference_category_testing">General</string>
|
||||
@@ -116,6 +129,7 @@
|
||||
<string name="preferences_copy_external_button">Copy external files directory</string>
|
||||
<string name="preferences_open_file_manager">Open file manager</string>
|
||||
<string name="preferences_view_logs">View application logs</string>
|
||||
<string name="preferences_view_crashes">View crash dumps</string>
|
||||
|
||||
<string name="preference_theme_name">Theme</string>
|
||||
<string name="preference_theme_select">Select theme</string>
|
||||
@@ -123,15 +137,43 @@
|
||||
<string name="preference_theme_light">Light</string>
|
||||
<string name="preference_theme_dark">Dark</string>
|
||||
<string name="preference_black_background_name">Black background in dark theme</string>
|
||||
<string name="preference_disable_styles">Disable custom styling</string>
|
||||
<string name="preference_set_application_icon">Application icon</string>
|
||||
<string name="preference_force_hrr">Force maximum refresh rate</string>
|
||||
<string name="preference_enable_redesign">✨ Enable UI refresh</string>
|
||||
|
||||
<string name="preference_display_mode_name">Screen scale</string>
|
||||
<string name="preference_display_mode_select">Select screen scale</string>
|
||||
<string name="preference_display_mode_legacy">Limited (16:9)</string>
|
||||
<string name="preference_display_mode_name">Display mode</string>
|
||||
<string name="preference_display_mode_select">Select display mode</string>
|
||||
<string name="preference_display_mode_legacy">Limited (Custom)</string>
|
||||
<string name="preference_display_mode_fullscreen">Edge-to-edge</string>
|
||||
<string name="preference_display_mode_default">Default</string>
|
||||
|
||||
<string name="preference_custom_aspect_ratio_name">Aspect ratio</string>
|
||||
<string name="preference_custom_aspect_ratio_description">Limits aspect ratio of the game.</string>
|
||||
<string name="preference_custom_aspect_ratio_select">Select aspect ratio</string>
|
||||
<string name="preference_custom_aspect_ratio_16x9">16:9</string>
|
||||
<string name="preference_custom_aspect_ratio_16x10">16:10</string>
|
||||
<string name="preference_custom_aspect_ratio_4x3">4:3</string>
|
||||
<string name="preference_custom_aspect_ratio_1x1">1:1</string>
|
||||
|
||||
<string name="preference_screen_zoom_name">Screen zoom</string>
|
||||
<string name="preference_screen_zoom_description">Reduces the size of the game window.</string>
|
||||
<string name="preference_screen_zoom_select">Edit screen zoom</string>
|
||||
<string name="preference_screen_zoom_fit_name">Fit to screen</string>
|
||||
|
||||
<string name="preference_limit_framerate_name">Limit max framerate</string>
|
||||
<string name="preference_limit_framerate_description">Limits framerate during rendering.</string>
|
||||
<string name="preference_limit_framerate_select">Edit maximum framerate</string>
|
||||
<string name="preference_limit_framerate_default">Disabled</string>
|
||||
<string name="preference_limit_framerate_label">Framerate</string>
|
||||
<string name="preference_limit_framerate_suffix">FPS</string>
|
||||
<string name="preference_limit_framerate_value">%1$d FPS</string>
|
||||
<string name="preference_limit_framerate_error_max">This device supports at most %1$s FPS.</string>
|
||||
<string name="preference_limit_framerate_error_min">Framerate must be at least %1$s FPS.</string>
|
||||
<string name="preference_limit_framerate_subtract">Decrement</string>
|
||||
<string name="preference_limit_framerate_add">Increment</string>
|
||||
<string name="preference_limit_framerate_reset">Disable</string>
|
||||
|
||||
<string name="preference_category_updater">Updates</string>
|
||||
<string name="preference_update_automatically_name">Enable automatic updates</string>
|
||||
<string name="preference_release_channel_name">Use nightly release channel</string>
|
||||
@@ -144,9 +186,29 @@
|
||||
<string name="preference_release_channel_beta">Pre-release</string>
|
||||
<string name="preference_release_channel_nightly">Nightly</string>
|
||||
|
||||
<string name="preference_category_about">About</string>
|
||||
|
||||
<string name="preference_launcher_version_name">Android Launcher</string>
|
||||
<string name="preference_launcher_version_description">v%1$s</string>
|
||||
<string name="preference_launcher_version">Android Launcher v%1$s</string>
|
||||
|
||||
<string name="preference_loader_version_name">Geode</string>
|
||||
<string name="preference_loader_version_description">%1$s</string>
|
||||
<string name="preference_loader_version">Geode %1$s</string>
|
||||
|
||||
<string name="preference_game_version_name">Geometry Dash</string>
|
||||
<string name="preference_game_version_description">%1$s (%2$s)</string>
|
||||
<string name="preference_game_version">Geometry Dash %1$s (%2$s)</string>
|
||||
<string name="preference_game_source_google">Google</string>
|
||||
<string name="preference_game_source_amazon">Amazon</string>
|
||||
<string name="preference_game_source_unknown">Unknown</string>
|
||||
|
||||
<string name="preference_architecture_name">Device</string>
|
||||
<string name="preference_architecture_description">%1$s (%2$s)</string>
|
||||
<string name="preference_architecture">%1$s (%2$s)</string>
|
||||
|
||||
<string name="preference_check_for_updates_button">Check for updates</string>
|
||||
<string name="preference_check_for_updates_description">Current loader version: %1$s</string>
|
||||
<string name="preference_launcher_version">Geode Launcher v%1$s</string>
|
||||
<string name="preference_check_for_updates_failed">Update failed. See logs for more details.</string>
|
||||
<string name="preference_check_for_updates_none_found">No updates found.</string>
|
||||
<string name="preference_check_for_updates_success">Geode has been updated!</string>
|
||||
@@ -160,27 +222,58 @@
|
||||
<string name="preference_text_clear">Clear</string>
|
||||
|
||||
<string name="preference_open_developer_options">Open developer options</string>
|
||||
<string name="preference_developer_options_dialog_title">Enable developer options?</string>
|
||||
|
||||
<string name="preference_profiles">Profiles</string>
|
||||
<string name="preference_profiles_create">Create profile</string>
|
||||
<string name="preference_profiles_name">Change profile</string>
|
||||
<string name="preference_profiles_select">Select profile</string>
|
||||
<string name="preference_profiles_create_name">Profile name</string>
|
||||
<string name="preference_profiles_create_action">Create</string>
|
||||
<string name="preference_profiles_select_value">%1$s</string>
|
||||
<string name="preference_profiles_current">Current profile: %1$s</string>
|
||||
<string name="preference_profiles_create_info">This profile will be created at %1$s.</string>
|
||||
<string name="preference_profiles_create_duplicate">A profile with this name already exists.</string>
|
||||
<string name="preference_profiles_delete_alt">Delete</string>
|
||||
<string name="preference_profiles_delete_undo_alt">Undo</string>
|
||||
|
||||
<!-- ApplicationLogsActivity strings -->
|
||||
<string name="title_activity_application_logs">Application Logs</string>
|
||||
<string name="title_activity_application_logs">Application logs</string>
|
||||
<string name="application_logs_no_logs">No logs available.</string>
|
||||
<string name="application_logs_delete">Clear All</string>
|
||||
<string name="application_logs_delete">Clear all</string>
|
||||
<string name="application_logs_share">Share</string>
|
||||
<string name="application_log_copy_label">Log Message</string>
|
||||
<string name="application_log_copy_label">Log message</string>
|
||||
<string name="application_logs_more">More</string>
|
||||
<string name="application_logs_export">Save To</string>
|
||||
<string name="application_logs_level">Set Log Level</string>
|
||||
<string name="application_logs_export">Save to</string>
|
||||
<string name="application_logs_level">Set log level</string>
|
||||
<string name="application_logs_level_dialog_name">Set minimum log level</string>
|
||||
<string name="application_logs_export_completed">Log export complete.</string>
|
||||
<string name="application_logs_default_filename" translatable="false">launcher_logcat.txt</string>
|
||||
<string name="application_logs_crash_only">Show Crash Buffer</string>
|
||||
<string name="application_logs_crash_only">Show crash buffer</string>
|
||||
<string name="application_logs_crash_only_enabled_alt">Enabled</string>
|
||||
|
||||
<string name="title_activity_developer_settings">Developer Settings</string>
|
||||
<!-- DeveloperSettingsActivity strings -->
|
||||
<string name="title_activity_developer_settings">Developer options</string>
|
||||
<string name="preference_developer_mode">Enable developer options</string>
|
||||
<string name="preference_developer_mode_about">Developer mode enables some options that may make the game unstable. Please do not modify them unless you know what you\'re doing!\nThese options may change behavior, or be removed entirely in future updates.</string>
|
||||
<string name="preference_developer_mode_about">Developer mode allows changing options that may make the game unstable. Please do not modify them unless you know what you\'re doing!\nThese options may change behavior, or be removed entirely in future updates.</string>
|
||||
<string name="preference_developer_update_notice">Enabling developer mode automatically disallows the updater from overwriting custom loader builds.</string>
|
||||
|
||||
<string name="preference_override_exceptions_name">Custom exceptions fix symbols</string>
|
||||
<string name="preference_override_exceptions_description">Uses exception_symbols.txt to patch</string>
|
||||
<string name="preference_use_index_update_api">Use index release API</string>
|
||||
|
||||
<!-- CrashLogsActivity strings -->
|
||||
<string name="title_activity_crash_logs">Crash dumps</string>
|
||||
<string name="application_crashes_no_dumps">No crash dumps available.</string>
|
||||
<string name="application_crashes_delete_one">Delete</string>
|
||||
|
||||
<!-- ApplicationIconActivity strings -->
|
||||
<string name="title_activity_application_icon">Application icons</string>
|
||||
<string name="application_icon_set_failed">Failed to change icon.</string>
|
||||
<string name="application_icon_default">Default</string>
|
||||
<string name="application_icon_geode">Geode</string>
|
||||
<string name="application_icon_pride">Pride</string>
|
||||
<string name="application_icon_trans">Transgender</string>
|
||||
|
||||
|
||||
</resources>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="Theme.NoActionBar.Fullscreen" parent="@style/Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
@@ -7,6 +7,7 @@
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowBackground">@android:drawable/screen_background_dark</item>
|
||||
<item name="android:windowOptOutEdgeToEdgeEnforcement" tools:targetApi="35">true</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.GeodeLauncher" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
|
||||
+6
-6
@@ -1,14 +1,14 @@
|
||||
buildscript {
|
||||
val composeBOM by extra("2024.06.00")
|
||||
val composeBOM by extra("2025.09.00")
|
||||
}
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id("com.android.application") version "8.5.0" apply false
|
||||
id("com.android.library") version "8.5.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.0.0" apply false
|
||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0" apply false
|
||||
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
|
||||
id("com.android.application") version "8.13.0" apply false
|
||||
id("com.android.library") version "8.13.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" apply false
|
||||
id("org.jetbrains.kotlin.plugin.compose") version "2.2.20" apply false
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-1
@@ -1,6 +1,7 @@
|
||||
#Thu Apr 24 10:54:01 MST 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -84,7 +86,8 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
Vendored
+2
@@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
|
||||
Reference in new issue
Block a user