Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0715c1bd8 | ||
|
|
41a5f560a3 | ||
|
|
3c43d8be22 | ||
|
|
f3d36259a1 | ||
|
|
6007fee0b8 | ||
|
|
afad922deb | ||
|
|
4df6da6521 | ||
|
|
d9b0b74762 | ||
|
|
a28e6d96ab | ||
|
|
c085409cee | ||
|
|
76c97a8d71 | ||
|
|
36fcbb5c52 | ||
|
|
e47f044189 | ||
|
|
426a1a1ade | ||
|
|
e786599e73 | ||
|
|
b08e2b4a3f | ||
|
|
41f663d55e | ||
|
|
b7d3d3e730 | ||
|
|
92d876560a | ||
|
|
b69c915e51 | ||
|
|
2b05f68af9 | ||
|
|
47d216b486 | ||
|
|
b4dd1567c9 | ||
|
|
8b2544434d | ||
|
|
24b0e39658 | ||
|
|
f763534a72 | ||
|
|
3887c3fe60 | ||
|
|
53a67f3e43 | ||
|
|
150bac781b | ||
|
|
0455a19f18 | ||
|
|
3a6c731710 | ||
|
|
2dde48c8fc | ||
|
|
f52b90b259 | ||
|
|
fa69a649dd | ||
|
|
a16c76cd06 | ||
|
|
9b5d614d1a | ||
|
|
d6a6de8361 | ||
|
|
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 |
No files matched your search
@@ -1,3 +1,18 @@
|
||||
# Geode Android Launcher
|
||||
|
||||
Launches a vanilla copy of Geometry Dash with the Geode loader added.
|
||||
|
||||
## Shortcut Links
|
||||
|
||||
The launcher supports a handful of links as an escape hatch when the main functionality is impaired in some way (such as needing to prevent an automatic launch due to a crash).
|
||||
You can open these links by copying them into the URL bar in some browsers (like Chrome), or through ADB.
|
||||
|
||||
| Link | Action |
|
||||
|---------------------------------------------|--------------------------------------------------------------------------|
|
||||
| geode-launcher://main | Opens to the main activity, forces the user to explicitly confirm launch |
|
||||
| geode-launcher://main/launch | Opens to the game after the launcher is finished checking for updates |
|
||||
| geode-launcher://main/launch?safe-mode=true | Opens to the game like before, but with safe mode enabled |
|
||||
| geode-launcher://settings | Opens the main settings |
|
||||
| geode-launcher://developer-settings | Opens the developer settings |
|
||||
| geode-launcher://logs | Opens the application logs |
|
||||
| geode-launcher://crashes | Opens the crash dumps viewer |
|
||||
@@ -1,3 +1,5 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
val composeBOM: String by rootProject.extra
|
||||
|
||||
kotlin {
|
||||
@@ -8,20 +10,19 @@ kotlin {
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("org.jetbrains.kotlin.plugin.serialization")
|
||||
id("org.jetbrains.kotlin.plugin.compose")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 35
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.geode.launcherXps"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0.0"
|
||||
applicationId = "com.geode.launcher"
|
||||
minSdk = 23
|
||||
targetSdk = 36
|
||||
versionCode = 23
|
||||
versionName = "1.7.0"
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
@@ -68,9 +69,6 @@ android {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
@@ -82,32 +80,34 @@ android {
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
version = "3.25.0+"
|
||||
path = file("src/main/cpp/CMakeLists.txt")
|
||||
}
|
||||
}
|
||||
namespace = "com.geode.launcher"
|
||||
ndkVersion = "27.2.12479018"
|
||||
ndkVersion = "29.0.14206865"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation (platform("androidx.compose:compose-bom:$composeBOM"))
|
||||
implementation ("androidx.core:core-ktx:1.15.0")
|
||||
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.7")
|
||||
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
|
||||
implementation ("androidx.activity:activity-compose:1.10.0")
|
||||
implementation ("androidx.activity:activity-ktx:1.10.0")
|
||||
implementation ("androidx.appcompat:appcompat:1.7.0")
|
||||
implementation ("androidx.documentfile:documentfile:1.0.1")
|
||||
implementation ("com.squareup.okio:okio:3.10.2")
|
||||
implementation ("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
|
||||
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.8.0")
|
||||
implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
|
||||
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.30.0")
|
||||
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.30.0")
|
||||
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
|
||||
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
|
||||
implementation ("androidx.activity:activity-compose:1.12.3")
|
||||
implementation ("androidx.activity:activity-ktx:1.12.3")
|
||||
implementation ("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation ("androidx.documentfile:documentfile:1.1.0")
|
||||
implementation ("com.squareup.okio:okio:3.16.4")
|
||||
implementation ("com.squareup.okhttp3:okhttp:5.3.2")
|
||||
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0")
|
||||
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.10.0")
|
||||
implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
|
||||
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.39.1")
|
||||
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.39.1")
|
||||
implementation ("androidx.browser:browser:1.9.0")
|
||||
debugImplementation ("androidx.compose.ui:ui-tooling")
|
||||
coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||
coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.1.5")
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<queries>
|
||||
<package android:name="xyz.lncvrt.xps0000002200" />
|
||||
<package android:name="com.robtopx.geometryjump" />
|
||||
</queries>
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@@ -21,7 +21,9 @@
|
||||
<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.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.LIGHTS" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.wifi"
|
||||
@@ -29,48 +31,90 @@
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00020000"
|
||||
android:required="true" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.gamepad"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:appCategory="game"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:hasFragileUserData="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:theme="@style/Theme.GeodeLauncher"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".AltMainActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.GeodeLauncher" />
|
||||
tools:targetApi="35">
|
||||
<receiver
|
||||
android:name=".InstallReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".preferences.DeveloperSettingsActivity"
|
||||
android:exported="false"
|
||||
android:exported="true"
|
||||
android:label="@string/title_activity_developer_settings"
|
||||
android:theme="@style/Theme.GeodeLauncher" />
|
||||
android:theme="@style/Theme.GeodeLauncher">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="geode-launcher" android:host="developer-settings" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".preferences.ApplicationLogsActivity"
|
||||
android:exported="false"
|
||||
android:exported="true"
|
||||
android:label="@string/title_activity_application_logs"
|
||||
android:theme="@style/Theme.GeodeLauncher">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="geode-launcher" android:host="logs" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".preferences.CrashLogsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/title_activity_crash_logs"
|
||||
android:theme="@style/Theme.GeodeLauncher">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="geode-launcher" android:host="crashes" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<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:exported="true"
|
||||
android:label="@string/title_activity_settings"
|
||||
android:theme="@style/Theme.GeodeLauncher">
|
||||
<meta-data
|
||||
android:name="android.app.lib_name"
|
||||
android:value="" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="geode-launcher" android:host="settings" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".GeometryDashActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:screenOrientation="landscape"
|
||||
android:theme="@style/Theme.NoActionBar.Fullscreen"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize"
|
||||
android:windowSoftInputMode="stateAlwaysHidden">
|
||||
<meta-data
|
||||
android:name="android.app.lib_name"
|
||||
@@ -85,11 +129,79 @@
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="geode-launcher" android:host="main" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityTrans"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher_trans"
|
||||
android:label="@string/app_name"
|
||||
android:targetActivity=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="geode-launcher" android:host="main" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
<activity-alias
|
||||
android:name=".MainActivityPride"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher_pride"
|
||||
android:label="@string/app_name"
|
||||
android:targetActivity=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="geode-launcher" android:host="main" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
<activity-alias
|
||||
android:name=".MainActivityGeode"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher_geode"
|
||||
android:label="@string/app_name"
|
||||
android:targetActivity=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="geode-launcher" android:host="main" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<provider
|
||||
android:name=".UserDirectoryProvider"
|
||||
android:authorities="com.geode.launcherXps.user"
|
||||
android:authorities="com.geode.launcher.user"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||
@@ -99,7 +211,7 @@
|
||||
</provider>
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="com.geode.launcherXps.fileprovider"
|
||||
android:authorities="com.geode.launcher.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -109,7 +109,7 @@ bool patch_symbol(std::uint32_t* hash_table, char* str_table, ElfW(Sym)* sym_tab
|
||||
// we found it! now go rename the symbol
|
||||
|
||||
#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;
|
||||
|
||||
@@ -5,11 +5,11 @@ import android.app.Activity
|
||||
import android.content.*
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import com.geode.launcher.R
|
||||
import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.closeIMEKeyboard
|
||||
import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.openIMEKeyboard
|
||||
import java.lang.ref.WeakReference
|
||||
@@ -61,33 +61,40 @@ object BaseRobTopActivity {
|
||||
|
||||
@JvmStatic
|
||||
fun openURL(url: String) {
|
||||
Log.d("MAIN", "Open URL")
|
||||
me.get()?.runOnUiThread {
|
||||
try {
|
||||
me.get()?.startActivity(
|
||||
Intent(
|
||||
"android.intent.action.VIEW",
|
||||
Uri.parse(url)
|
||||
)
|
||||
)
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
me.get()?.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(
|
||||
me.get(),
|
||||
"No activity found to open this URL.",
|
||||
R.string.no_activity_found,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun copyToClipboard(str: String) {
|
||||
val context = me.get() ?: return
|
||||
context.runOnUiThread {
|
||||
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboardManager.setPrimaryClip(
|
||||
ClipData.newPlainText("text", str)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun sendMail(subject: String, body: String, to: String) {
|
||||
me.get()?.runOnUiThread {
|
||||
val i = Intent("android.intent.action.SEND")
|
||||
val i = Intent(Intent.ACTION_SEND)
|
||||
i.type = "message/rfc822"
|
||||
i.putExtra("android.intent.extra.EMAIL", arrayOf(to))
|
||||
i.putExtra("android.intent.extra.SUBJECT", subject)
|
||||
i.putExtra("android.intent.extra.TEXT", body)
|
||||
i.putExtra(Intent.EXTRA_EMAIL, arrayOf(to))
|
||||
i.putExtra(Intent.EXTRA_SUBJECT, subject)
|
||||
i.putExtra(Intent.EXTRA_TEXT, body)
|
||||
try {
|
||||
me.get()?.startActivity(Intent.createChooser(i, "Send mail..."))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
|
||||
@@ -1,522 +0,0 @@
|
||||
package com.geode.launcher
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.format.Formatter
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
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.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.utils.Constants
|
||||
import com.geode.launcher.utils.GamePackageUtils
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AltMainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
clearDownloadedApks(this)
|
||||
|
||||
val gdInstalled = GamePackageUtils.isGameInstalled(packageManager)
|
||||
val geodeInstalled = LaunchUtils.isGeodeInstalled(this)
|
||||
|
||||
val returnMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE)
|
||||
val returnExtendedMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE)
|
||||
|
||||
@Suppress("DEPRECATION") // the new api is android 13 only (why)
|
||||
val returnError = intent.extras?.getSerializable(LaunchUtils.LAUNCHER_KEY_RETURN_ERROR)
|
||||
as? LaunchUtils.LauncherError
|
||||
|
||||
val loadFailureInfo = if (returnError != null && returnMessage != null) {
|
||||
LoadFailureInfo(returnError, returnMessage, returnExtendedMessage)
|
||||
} else if (LaunchUtils.lastSessionCrashed(this)) {
|
||||
LoadFailureInfo(LaunchUtils.LauncherError.CRASHED)
|
||||
} else { null }
|
||||
|
||||
|
||||
setContent {
|
||||
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
|
||||
val theme = Theme.fromInt(themeOption)
|
||||
|
||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||
|
||||
val launchViewModel = viewModel<LaunchViewModel>(factory = LaunchViewModel.Factory)
|
||||
|
||||
if (loadFailureInfo != null) {
|
||||
launchViewModel.loadFailure = loadFailureInfo
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
AltMainScreen(launchViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (gdInstalled && geodeInstalled) {
|
||||
intent.getBooleanExtra("restarted", false).let {
|
||||
if (it) {
|
||||
onLaunch(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class LaunchStatusInfo(
|
||||
val title: String?,
|
||||
val details: String? = null,
|
||||
val progress: (() -> Float)? = null
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun LaunchCancelledIcon(cancelReason: LaunchViewModel.LaunchCancelReason, modifier: Modifier = Modifier) {
|
||||
when (cancelReason) {
|
||||
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED,
|
||||
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> Icon(painterResource(R.drawable.icon_error), contentDescription = null, modifier)
|
||||
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED,
|
||||
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> Icon(Icons.Default.Warning, contentDescription = null, modifier)
|
||||
else -> Icon(Icons.Default.Info, contentDescription = null, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun mapCancelReasonToInfo(cancelReason: LaunchViewModel.LaunchCancelReason): LaunchStatusInfo {
|
||||
return when (cancelReason) {
|
||||
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> LaunchStatusInfo(
|
||||
title = stringResource(R.string.launcher_cancelled_crash)
|
||||
)
|
||||
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> LaunchStatusInfo(
|
||||
title = stringResource(R.string.geode_download_title)
|
||||
)
|
||||
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> LaunchStatusInfo(
|
||||
title = stringResource(R.string.launcher_cancelled_error),
|
||||
details = stringResource(R.string.game_not_found)
|
||||
)
|
||||
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> LaunchStatusInfo(
|
||||
title = stringResource(R.string.launcher_cancelled_error),
|
||||
details = stringResource(R.string.launcher_cancelled_outdated)
|
||||
)
|
||||
LaunchViewModel.LaunchCancelReason.AUTOMATIC,
|
||||
LaunchViewModel.LaunchCancelReason.MANUAL-> LaunchStatusInfo(
|
||||
title = null
|
||||
)
|
||||
else -> LaunchStatusInfo(
|
||||
title = stringResource(R.string.launcher_cancelled_manual)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun mapLaunchStatusToInfo(state: LaunchViewModel.LaunchUIState, inSafeMode: Boolean = false): LaunchStatusInfo {
|
||||
return when (state) {
|
||||
is LaunchViewModel.LaunchUIState.Initial,
|
||||
is LaunchViewModel.LaunchUIState.Working,
|
||||
is LaunchViewModel.LaunchUIState.Ready -> LaunchStatusInfo(
|
||||
title = stringResource(R.string.launcher_starting_game),
|
||||
details = if (inSafeMode) stringResource(R.string.launcher_in_safe_mode) else null
|
||||
)
|
||||
is LaunchViewModel.LaunchUIState.UpdateCheck -> LaunchStatusInfo(
|
||||
title = stringResource(R.string.release_fetch_in_progress)
|
||||
)
|
||||
is LaunchViewModel.LaunchUIState.Updating -> {
|
||||
val context = LocalContext.current
|
||||
|
||||
val downloaded = remember(state.downloaded) {
|
||||
Formatter.formatShortFileSize(context, state.downloaded)
|
||||
}
|
||||
|
||||
val outOf = remember(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 ?: "…"),
|
||||
progress = {
|
||||
val progress = state.downloaded / state.outOf!!.toDouble()
|
||||
progress.toFloat()
|
||||
}.takeIf { state.outOf != null }
|
||||
)
|
||||
}
|
||||
is LaunchViewModel.LaunchUIState.Cancelled -> {
|
||||
return mapCancelReasonToInfo(state.reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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.CenterHorizontally,
|
||||
modifier = modifier.width(300.dp)
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inProgress) {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
|
||||
if (statusInfo.details != null) {
|
||||
Text(
|
||||
statusInfo.details,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LaunchProgressBody(statusInfo: LaunchStatusInfo, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = modifier.width(300.dp)
|
||||
) {
|
||||
if (statusInfo.title != null) {
|
||||
Text(statusInfo.title, style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
|
||||
if (statusInfo.progress != null) {
|
||||
LinearProgressIndicator(
|
||||
statusInfo.progress,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
} else {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
|
||||
if (statusInfo.details != null) {
|
||||
Text(statusInfo.details, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RetryButtonContents(reason: LaunchViewModel.LaunchCancelReason) {
|
||||
when (reason) {
|
||||
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_resume),
|
||||
contentDescription = null
|
||||
)
|
||||
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,
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_cancelled_retry))
|
||||
}
|
||||
// these shouldn't be allowing retries anyways
|
||||
LaunchViewModel.LaunchCancelReason.GAME_MISSING,
|
||||
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun LaunchProgressCard(
|
||||
uiState: LaunchViewModel.LaunchUIState,
|
||||
crashInfo: LoadFailureInfo?,
|
||||
onCancel: () -> Unit,
|
||||
onResume: (safeMode: Boolean) -> Unit,
|
||||
onMore: () -> Unit,
|
||||
extraOptions: @Composable () -> Unit,
|
||||
safeModeEnabled: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var showSafeModeDialog by remember { mutableStateOf(false) }
|
||||
val status = mapLaunchStatusToInfo(uiState, safeModeEnabled)
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
if (uiState is LaunchViewModel.LaunchUIState.Cancelled) {
|
||||
LaunchCancelledBody(
|
||||
statusInfo = status,
|
||||
icon = { LaunchCancelledIcon(uiState.reason, Modifier.size(32.dp)) },
|
||||
inProgress = uiState.inProgress
|
||||
)
|
||||
|
||||
if (uiState.reason.isGameInstallIssue()) {
|
||||
val context = LocalContext.current
|
||||
val showDownload = remember { GamePackageUtils.showDownloadBadge(context.packageManager) }
|
||||
|
||||
if (showDownload) {
|
||||
GooglePlayBadge(modifier = Modifier.align(Alignment.CenterHorizontally))
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.reason.allowsRetry()) {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
LongPressButton(onClick = { onResume(false) }, onLongPress = {
|
||||
showSafeModeDialog = true
|
||||
}) {
|
||||
RetryButtonContents(uiState.reason)
|
||||
}
|
||||
|
||||
if (crashInfo != null) {
|
||||
FilledTonalButton(onClick = onMore) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_question_mark),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_cancelled_help_alt))
|
||||
}
|
||||
}
|
||||
|
||||
extraOptions()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LaunchProgressBody(statusInfo = status, modifier = Modifier.padding(top = 16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.width(300.dp)) {
|
||||
TextButton(onClick = onCancel) {
|
||||
Text(stringResource(R.string.release_fetch_button_cancel))
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1.0f))
|
||||
|
||||
extraOptions()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showSafeModeDialog) {
|
||||
SafeModeDialog(
|
||||
onDismiss = {
|
||||
showSafeModeDialog = false
|
||||
},
|
||||
onLaunch = {
|
||||
onResume(true)
|
||||
showSafeModeDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtraOptions(onSettings: () -> Unit) {
|
||||
IconButton(
|
||||
onClick = onSettings,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Settings,
|
||||
contentDescription = stringResource(R.string.launcher_settings_icon_alt)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GeodeLogo(modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.geode_logo),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(76.dp, 76.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.launcher_title),
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
modifier = Modifier.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AltMainScreen(
|
||||
launchViewModel: LaunchViewModel = viewModel(factory = LaunchViewModel.Factory)
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launchViewModel.beginLaunchFlow()
|
||||
}
|
||||
|
||||
val launchUIState by launchViewModel.uiState.collectAsState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var launchInSafeMode by remember { mutableStateOf(false) }
|
||||
|
||||
var showErrorInfo by remember { mutableStateOf(false) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.safeDrawingPadding(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
GeodeLogo()
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
LaunchProgressCard(
|
||||
launchUIState,
|
||||
crashInfo = launchViewModel.currentCrashInfo(),
|
||||
onCancel = {
|
||||
launchInSafeMode = false
|
||||
coroutineScope.launch {
|
||||
launchViewModel.cancelLaunch()
|
||||
}
|
||||
},
|
||||
onResume = { safeMode ->
|
||||
launchInSafeMode = safeMode
|
||||
|
||||
launchViewModel.clearCrashInfo()
|
||||
coroutineScope.launch {
|
||||
launchViewModel.beginLaunchFlow(true)
|
||||
}
|
||||
},
|
||||
onMore = {
|
||||
showErrorInfo = true
|
||||
},
|
||||
extraOptions = {
|
||||
ExtraOptions(
|
||||
onSettings = {
|
||||
coroutineScope.launch {
|
||||
launchViewModel.cancelLaunch(true)
|
||||
}
|
||||
|
||||
onSettings(context)
|
||||
}
|
||||
)
|
||||
},
|
||||
safeModeEnabled = launchInSafeMode
|
||||
)
|
||||
|
||||
// only show launcher update in a case where a user won't see it ingame
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (launchUIState is LaunchViewModel.LaunchUIState.Ready) {
|
||||
UpdateWarning(launchInSafeMode) {
|
||||
launchInSafeMode = false
|
||||
coroutineScope.launch {
|
||||
launchViewModel.cancelLaunch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val loadFailure = launchViewModel.currentCrashInfo()
|
||||
if (showErrorInfo && loadFailure != null) {
|
||||
ErrorInfoSheet(loadFailure, onDismiss = { showErrorInfo = false })
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,18 @@ import android.os.Environment
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowInsets
|
||||
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
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import com.customRobTop.BaseRobTopActivity
|
||||
import com.customRobTop.JniToCpp
|
||||
import com.geode.launcher.main.LaunchNotification
|
||||
@@ -31,7 +35,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
|
||||
@@ -121,6 +124,13 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
mGLSurfaceView?.sendKeyBack()
|
||||
}
|
||||
})
|
||||
mGLSurfaceView?.manualBackEvents = true
|
||||
}
|
||||
|
||||
private fun createVersionFile() {
|
||||
@@ -177,7 +187,34 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
|
||||
loadInternalMods()
|
||||
|
||||
setContentView(createView())
|
||||
val baseView = createView()
|
||||
|
||||
if (!GeodeUtils.handleSafeArea && Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
|
||||
val frameLayoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
val frameLayout = FrameLayout(this)
|
||||
frameLayout.layoutParams = frameLayoutParams
|
||||
|
||||
frameLayout.setOnApplyWindowInsetsListener { v, insets ->
|
||||
val displayInsets = insets.displayCutout
|
||||
if (displayInsets != null) {
|
||||
v.updatePadding(
|
||||
left = displayInsets.safeInsetLeft,
|
||||
top = displayInsets.safeInsetTop,
|
||||
right = displayInsets.safeInsetRight,
|
||||
bottom = displayInsets.safeInsetBottom
|
||||
)
|
||||
}
|
||||
WindowInsets.CONSUMED
|
||||
}
|
||||
|
||||
frameLayout.addView(baseView)
|
||||
setContentView(frameLayout)
|
||||
} else {
|
||||
setContentView(baseView)
|
||||
}
|
||||
|
||||
setupPostLibraryLoad(gdPackageInfo)
|
||||
|
||||
@@ -398,7 +435,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
val editText = Cocos2dxEditText(this)
|
||||
val editText = AppCompatEditText(this)
|
||||
editText.layoutParams = editTextLayoutParams
|
||||
frameLayout.addView(editText)
|
||||
|
||||
@@ -407,8 +444,8 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
this.mGLSurfaceView = glSurfaceView
|
||||
frameLayout.addView(this.mGLSurfaceView)
|
||||
|
||||
val showNotification = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.ENABLE_REDESIGN)
|
||||
if (showNotification) {
|
||||
val disableNotification = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.DISABLE_REDESIGN)
|
||||
if (!disableNotification) {
|
||||
val hasCards = determineDisplayedCards(this)
|
||||
if (hasCards.isNotEmpty()) {
|
||||
val notificationView = ComposeView(this)
|
||||
@@ -435,7 +472,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
renderer.limitFrameRate(frameRate)
|
||||
}
|
||||
|
||||
editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||
editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
||||
glSurfaceView.cocos2dxEditText = editText
|
||||
|
||||
return frameLayout
|
||||
@@ -457,11 +494,11 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
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) {
|
||||
mForceRefreshRate = true
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val chosenDisplay = display.supportedModes?.maxByOrNull { it.refreshRate }
|
||||
if (chosenDisplay != null) {
|
||||
var chosenRefreshRate = chosenDisplay.refreshRate
|
||||
val chosenRefreshRate = chosenDisplay.refreshRate
|
||||
println("setupUIState: Forcing a refresh rate of $chosenRefreshRate (display ${chosenDisplay.modeId})")
|
||||
|
||||
window.attributes.preferredRefreshRate = chosenRefreshRate
|
||||
@@ -472,6 +509,9 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
|
||||
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()
|
||||
@@ -577,6 +617,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
||||
mGLSurfaceView?.sendTimestampEvents = true
|
||||
true
|
||||
}
|
||||
GeodeUtils.CAPABILITY_RICH_INPUT -> {
|
||||
mGLSurfaceView?.enableControllerEvents()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.geode.launcher
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.widget.Toast
|
||||
import com.geode.launcher.main.clearDownloadedApks
|
||||
|
||||
class InstallReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val activityIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
||||
|
||||
if (activityIntent != null) {
|
||||
context.startActivity(activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
}
|
||||
PackageInstaller.STATUS_SUCCESS -> {
|
||||
clearDownloadedApks(context)
|
||||
}
|
||||
else -> {
|
||||
val msg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
println("failed to install update: $status, $msg")
|
||||
|
||||
val message = context.getString(R.string.launcher_self_update_failed, msg)
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,52 @@
|
||||
package com.geode.launcher
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import android.os.Bundle
|
||||
import android.text.format.Formatter
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -19,36 +54,58 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.CompositingStrategy
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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.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.Theme.DARK
|
||||
import com.geode.launcher.ui.theme.Theme.LIGHT
|
||||
import com.geode.launcher.ui.theme.launcherTitleStyle
|
||||
import com.geode.launcher.updater.ReleaseManager
|
||||
import com.geode.launcher.utils.Constants
|
||||
import com.geode.launcher.utils.GamePackageUtils
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import com.geode.launcher.utils.GamePackageUtils
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.ConnectException
|
||||
import java.net.UnknownHostException
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
clearDownloadedApks(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val gdInstalled = GamePackageUtils.isGameInstalled(packageManager)
|
||||
val geodeInstalled = LaunchUtils.isGeodeInstalled(this)
|
||||
|
||||
if (gdInstalled && geodeInstalled && intent.getBooleanExtra("restarted", false)) {
|
||||
onLaunch(this)
|
||||
}
|
||||
|
||||
enableEdgeToEdge()
|
||||
clearDownloadedApks(this)
|
||||
|
||||
val returnMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE)
|
||||
val returnExtendedMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE)
|
||||
|
||||
@@ -62,13 +119,19 @@ class MainActivity : ComponentActivity() {
|
||||
LoadFailureInfo(LaunchUtils.LauncherError.CRASHED)
|
||||
} else { null }
|
||||
|
||||
val redirectToAlt = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.ENABLE_REDESIGN)
|
||||
if (redirectToAlt) {
|
||||
val launchIntent = Intent(this, AltMainActivity::class.java)
|
||||
launchIntent.putExtras(intent)
|
||||
startActivity(launchIntent)
|
||||
var launchArguments: LaunchArguments? = null
|
||||
val intentData = intent.data
|
||||
if (intent.action == Intent.ACTION_VIEW && intentData != null) {
|
||||
val isLaunch = intentData.path == "/launch"
|
||||
val safeMode = if (isLaunch) {
|
||||
intentData.getBooleanQueryParameter("safe-mode", false)
|
||||
} else false
|
||||
|
||||
return
|
||||
launchArguments = LaunchArguments(
|
||||
autoSafeMode = safeMode,
|
||||
forcePause = !isLaunch,
|
||||
forceLaunch = isLaunch,
|
||||
)
|
||||
}
|
||||
|
||||
setContent {
|
||||
@@ -76,196 +139,630 @@ 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)
|
||||
|
||||
val launchViewModel = viewModel<LaunchViewModel>(factory = LaunchViewModel.Factory)
|
||||
|
||||
if (loadFailureInfo != null) {
|
||||
launchViewModel.loadFailure = loadFailureInfo
|
||||
}
|
||||
|
||||
if (launchArguments != null) {
|
||||
launchViewModel.launchArguments = launchArguments
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
MainScreen(gdInstalled, geodeInstalled, loadFailureInfo)
|
||||
AltMainScreen(launchViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (gdInstalled && geodeInstalled) {
|
||||
intent.getBooleanExtra("restarted", false).let {
|
||||
if (it) {
|
||||
onLaunch(this)
|
||||
}
|
||||
}
|
||||
|
||||
data class LaunchStatusInfo(
|
||||
val title: String?,
|
||||
val details: String? = null,
|
||||
val progress: (() -> Float)? = null
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun LaunchCancelledIcon(cancelReason: LaunchViewModel.LaunchCancelReason, modifier: Modifier = Modifier) {
|
||||
when (cancelReason) {
|
||||
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED,
|
||||
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> Icon(painterResource(R.drawable.icon_error), contentDescription = null, modifier)
|
||||
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED,
|
||||
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> Icon(painterResource(R.drawable.icon_warning), contentDescription = null, modifier)
|
||||
else -> Icon(painterResource(R.drawable.icon_info), contentDescription = null, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun mapCancelReasonToInfo(cancelReason: LaunchViewModel.LaunchCancelReason): LaunchStatusInfo {
|
||||
return when (cancelReason) {
|
||||
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> LaunchStatusInfo(
|
||||
title = stringResource(R.string.launcher_cancelled_crash)
|
||||
)
|
||||
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> LaunchStatusInfo(
|
||||
title = stringResource(R.string.geode_download_title)
|
||||
)
|
||||
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> LaunchStatusInfo(
|
||||
title = stringResource(R.string.launcher_cancelled_error),
|
||||
details = stringResource(R.string.game_not_found)
|
||||
)
|
||||
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> LaunchStatusInfo(
|
||||
title = stringResource(R.string.launcher_cancelled_error),
|
||||
details = stringResource(R.string.launcher_cancelled_outdated)
|
||||
)
|
||||
LaunchViewModel.LaunchCancelReason.AUTOMATIC,
|
||||
LaunchViewModel.LaunchCancelReason.MANUAL-> LaunchStatusInfo(
|
||||
title = null
|
||||
)
|
||||
/*
|
||||
else -> LaunchStatusInfo(
|
||||
title = stringResource(R.string.launcher_cancelled_manual)
|
||||
)
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun mapLaunchStatusToInfo(state: LaunchViewModel.LaunchUIState, inSafeMode: Boolean = false): LaunchStatusInfo {
|
||||
return when (state) {
|
||||
is LaunchViewModel.LaunchUIState.Initial,
|
||||
is LaunchViewModel.LaunchUIState.Working,
|
||||
is LaunchViewModel.LaunchUIState.Ready -> LaunchStatusInfo(
|
||||
title = stringResource(R.string.launcher_starting_game),
|
||||
details = if (inSafeMode) stringResource(R.string.launcher_in_safe_mode) else null
|
||||
)
|
||||
is LaunchViewModel.LaunchUIState.UpdateCheck -> LaunchStatusInfo(
|
||||
title = stringResource(R.string.release_fetch_in_progress)
|
||||
)
|
||||
is LaunchViewModel.LaunchUIState.Updating -> {
|
||||
val context = LocalContext.current
|
||||
|
||||
val downloaded = remember(state.downloaded) {
|
||||
Formatter.formatShortFileSize(context, state.downloaded)
|
||||
}
|
||||
|
||||
val outOf = remember(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 ?: "…"),
|
||||
progress = {
|
||||
val progress = state.downloaded / state.outOf!!.toDouble()
|
||||
progress.toFloat()
|
||||
}.takeIf { state.outOf != null }
|
||||
)
|
||||
}
|
||||
is LaunchViewModel.LaunchUIState.Cancelled -> {
|
||||
return mapCancelReasonToInfo(state.reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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.CenterHorizontally,
|
||||
modifier = modifier.width(300.dp)
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inProgress) {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
|
||||
if (statusInfo.details != null) {
|
||||
Text(
|
||||
statusInfo.details,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LaunchProgressBody(statusInfo: LaunchStatusInfo, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = modifier.width(300.dp)
|
||||
) {
|
||||
if (statusInfo.title != null) {
|
||||
Text(statusInfo.title, style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
|
||||
if (statusInfo.progress != null) {
|
||||
LinearProgressIndicator(
|
||||
statusInfo.progress,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
} else {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
|
||||
if (statusInfo.details != null) {
|
||||
Text(statusInfo.details, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RetryButtonContents(reason: LaunchViewModel.LaunchCancelReason) {
|
||||
when (reason) {
|
||||
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_resume),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_cancelled_resume))
|
||||
}
|
||||
LaunchViewModel.LaunchCancelReason.MANUAL,
|
||||
LaunchViewModel.LaunchCancelReason.AUTOMATIC -> {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_play_arrow),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_launch))
|
||||
}
|
||||
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_refresh),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_cancelled_retry))
|
||||
}
|
||||
// these shouldn't be allowing retries anyways
|
||||
LaunchViewModel.LaunchCancelReason.GAME_MISSING,
|
||||
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun LaunchProgressCard(
|
||||
uiState: LaunchViewModel.LaunchUIState,
|
||||
crashInfo: LoadFailureInfo?,
|
||||
onCancel: () -> Unit,
|
||||
onResume: (safeMode: Boolean) -> Unit,
|
||||
onMore: () -> Unit,
|
||||
extraOptions: @Composable () -> Unit,
|
||||
safeModeEnabled: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var showSafeModeDialog by remember { mutableStateOf(false) }
|
||||
val status = mapLaunchStatusToInfo(uiState, safeModeEnabled)
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
if (uiState is LaunchViewModel.LaunchUIState.Cancelled) {
|
||||
LaunchCancelledBody(
|
||||
statusInfo = status,
|
||||
icon = { LaunchCancelledIcon(uiState.reason, Modifier.size(32.dp)) },
|
||||
inProgress = uiState.inProgress
|
||||
)
|
||||
|
||||
if (uiState.reason.isGameInstallIssue()) {
|
||||
val context = LocalContext.current
|
||||
val showDownload = remember { GamePackageUtils.showDownloadBadge(context.packageManager) }
|
||||
|
||||
if (showDownload) {
|
||||
GooglePlayBadge(modifier = Modifier.align(Alignment.CenterHorizontally))
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.reason.allowsRetry()) {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
LongPressButton(onClick = { onResume(false) }, onLongPress = {
|
||||
showSafeModeDialog = true
|
||||
}) {
|
||||
RetryButtonContents(uiState.reason)
|
||||
}
|
||||
|
||||
if (crashInfo != null) {
|
||||
FilledTonalButton(onClick = onMore) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_question_mark),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_cancelled_help_alt))
|
||||
}
|
||||
}
|
||||
|
||||
extraOptions()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LaunchProgressBody(statusInfo = status, modifier = Modifier.padding(top = 16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.width(300.dp)) {
|
||||
TextButton(onClick = onCancel) {
|
||||
Text(stringResource(R.string.release_fetch_button_cancel))
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1.0f))
|
||||
|
||||
extraOptions()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showSafeModeDialog) {
|
||||
SafeModeDialog(
|
||||
onDismiss = {
|
||||
showSafeModeDialog = false
|
||||
},
|
||||
onLaunch = {
|
||||
onResume(true)
|
||||
showSafeModeDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtraOptions(onSettings: () -> Unit) {
|
||||
IconButton(
|
||||
onClick = onSettings,
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_settings),
|
||||
contentDescription = stringResource(R.string.launcher_settings_icon_alt)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val lightOuterColorGradient = arrayOf(
|
||||
0.0f to Color(0xfff5b11d),
|
||||
0.10f to Color(0xfff0834d),
|
||||
0.24f to Color(0xffe85f6b),
|
||||
0.49f to Color(0xffc9659e),
|
||||
1.0f to Color(0xffb9588f)
|
||||
)
|
||||
|
||||
val lightInnerColorGradient = arrayOf(
|
||||
0.48f to Color(0xfff2ce00),
|
||||
0.63f to Color(0xfff4b97e),
|
||||
0.73f to Color(0xfff7b66a),
|
||||
1.0f to Color(0xffeb8fac)
|
||||
)
|
||||
|
||||
val darkOuterColorGradient = arrayOf(
|
||||
0.0f to Color(0xfffffdff),
|
||||
0.08f to Color(0xffF2EAF5),
|
||||
0.12f to Color(0xffEDE5EF),
|
||||
0.31f to Color(0xffCDB5CD),
|
||||
0.49f to Color(0xffBA9BBC),
|
||||
1.0f to Color(0xff8D7ACF),
|
||||
)
|
||||
|
||||
val darkInnerColorGradient = arrayOf(
|
||||
0.48f to Color(0xffffffff),
|
||||
0.63f to Color(0xfff5e4c2),
|
||||
0.73f to Color(0xffe5c7ad),
|
||||
1.0f to Color(0xffb790a9),
|
||||
)
|
||||
|
||||
@Preview(name = "non-animated, light")
|
||||
@Preview(name = "non-animated, dark", uiMode = UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
fun GeodeLogoPreview() {
|
||||
val theme = if (isSystemInDarkTheme()) DARK else LIGHT
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme) {
|
||||
Surface {
|
||||
GeodeLogo()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "animated, light")
|
||||
@Preview(name = "animated, dark", uiMode = UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
fun GeodeLogoAnimatedPreview() {
|
||||
val theme = if (isSystemInDarkTheme()) DARK else LIGHT
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme) {
|
||||
Surface {
|
||||
GeodeLogo(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
gdInstalled: Boolean = true,
|
||||
geodePreinstalled: Boolean = true,
|
||||
loadFailureInfo: LoadFailureInfo? = null,
|
||||
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val shouldUpdate by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
|
||||
val autoUpdateState by releaseViewModel.uiState.collectAsState()
|
||||
|
||||
val geodeJustInstalled = (autoUpdateState as? ReleaseViewModel.ReleaseUIState.Finished)
|
||||
?.hasUpdated == true
|
||||
val geodeInstalled = geodePreinstalled || geodeJustInstalled
|
||||
|
||||
var beginLaunch by remember { mutableStateOf(false) }
|
||||
var launchInSafeMode by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(shouldUpdate) {
|
||||
if (shouldUpdate && !releaseViewModel.hasPerformedCheck && gdInstalled) {
|
||||
releaseViewModel.runReleaseCheck()
|
||||
} else {
|
||||
releaseViewModel.useGlobalCheckState()
|
||||
}
|
||||
fun AnimatedLogo(modifier: Modifier = Modifier) {
|
||||
val theme = LocalTheme.current
|
||||
val innerColorPalette = remember(theme) {
|
||||
if (theme == LIGHT) lightInnerColorGradient else darkInnerColorGradient
|
||||
}
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
// automatically show load failed dialog (but not crash)
|
||||
var showErrorInfo by remember { mutableStateOf(
|
||||
loadFailureInfo != null && loadFailureInfo.title != LaunchUtils.LauncherError.CRASHED
|
||||
)}
|
||||
|
||||
val hasError = loadFailureInfo != null
|
||||
LaunchedEffect(hasError, showErrorInfo) {
|
||||
if (!showErrorInfo && loadFailureInfo != null) {
|
||||
showFailureSnackbar(context, loadFailureInfo, snackbarHostState) {
|
||||
showErrorInfo = true
|
||||
}
|
||||
}
|
||||
val outerColorPalette = remember(theme) {
|
||||
if (theme == LIGHT) lightOuterColorGradient else darkOuterColorGradient
|
||||
}
|
||||
|
||||
if (showErrorInfo && loadFailureInfo != null) {
|
||||
ErrorInfoSheet(loadFailureInfo, onDismiss = { showErrorInfo = false })
|
||||
}
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
|
||||
val offset by infiniteTransition.animateFloat(
|
||||
initialValue = -1.0f,
|
||||
targetValue = 1.0f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(2000, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
),
|
||||
label = "gradient offset"
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
val innerBrush = Brush.linearGradient(
|
||||
colorStops = innerColorPalette,
|
||||
start = Offset(0.0f, 0.0f + (165.06f * offset)),
|
||||
end = Offset(0.0f, 165.06f + (165.06f * offset)),
|
||||
tileMode = TileMode.Mirror
|
||||
)
|
||||
|
||||
val outerBrush = Brush.linearGradient(
|
||||
colorStops = outerColorPalette,
|
||||
start = Offset(0.0f + (84.69f * offset), 0.0f + (165.06f * offset)),
|
||||
end = Offset(84.69f + (84.69f * offset), 169.61f + (165.06f * offset)),
|
||||
tileMode = TileMode.Mirror
|
||||
)
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.geode_monochrome_inner),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.geode_logo),
|
||||
contentDescription = context.getString(R.string.launcher_logo_alt),
|
||||
modifier = Modifier.size(100.dp, 100.dp)
|
||||
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
drawRect(
|
||||
brush = innerBrush,
|
||||
blendMode = BlendMode.SrcIn
|
||||
)
|
||||
Text(
|
||||
context.getString(R.string.launcher_title),
|
||||
fontSize = 32.sp,
|
||||
modifier = Modifier.padding(12.dp)
|
||||
}
|
||||
)
|
||||
|
||||
when {
|
||||
gdInstalled && geodeInstalled -> {
|
||||
val gdVersion = remember {
|
||||
GamePackageUtils.getGameVersionCode(context.packageManager)
|
||||
}
|
||||
|
||||
if (gdVersion < Constants.SUPPORTED_VERSION_CODE_MIN) {
|
||||
val versionName = remember {
|
||||
GamePackageUtils.getUnifiedVersionName(context.packageManager)
|
||||
}
|
||||
|
||||
LaunchBlockedLabel(stringResource(R.string.game_outdated, versionName))
|
||||
} else {
|
||||
val stopLaunch = releaseViewModel.isInUpdate || hasError
|
||||
PlayButton(
|
||||
stopAutomaticLaunch = stopLaunch,
|
||||
blockLaunch = releaseViewModel.isInUpdate,
|
||||
onPlayGame = { safeMode ->
|
||||
launchInSafeMode = safeMode
|
||||
beginLaunch = true
|
||||
},
|
||||
)
|
||||
|
||||
if (gdVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
|
||||
UnsupportedVersionWarning()
|
||||
}
|
||||
}
|
||||
}
|
||||
gdInstalled -> {
|
||||
Text(
|
||||
context.getString(R.string.geode_download_title),
|
||||
modifier = Modifier.padding(12.dp)
|
||||
)
|
||||
|
||||
Row {
|
||||
Button(
|
||||
onClick = { releaseViewModel.runReleaseCheck(true) },
|
||||
enabled = !releaseViewModel.isInUpdate
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_download),
|
||||
contentDescription = context.getString(R.string.launcher_download_icon_alt)
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(context.getString(R.string.launcher_download))
|
||||
}
|
||||
Spacer(Modifier.size(2.dp))
|
||||
IconButton(onClick = { onSettings(context) }) {
|
||||
Icon(
|
||||
Icons.Filled.Settings,
|
||||
contentDescription = context.getString(R.string.launcher_settings_icon_alt)
|
||||
painter = painterResource(id = R.drawable.geode_monochrome_outer),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
drawRect(
|
||||
brush = outerBrush,
|
||||
blendMode = BlendMode.SrcIn
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> LaunchBlockedLabel(stringResource(R.string.game_not_found))
|
||||
}
|
||||
|
||||
UpdateCard(
|
||||
releaseViewModel,
|
||||
@Composable
|
||||
fun GeodeLogo(shouldAnimate: Boolean = false, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
) {
|
||||
val theme = LocalTheme.current
|
||||
|
||||
Crossfade(
|
||||
targetState = shouldAnimate,
|
||||
animationSpec = tween(durationMillis = 1000, easing = FastOutSlowInEasing),
|
||||
label="logo fade"
|
||||
) { screen ->
|
||||
when (screen) {
|
||||
true -> AnimatedLogo(modifier = Modifier.size(64.dp, 64.dp))
|
||||
false -> Image(
|
||||
painterResource(if (theme == LIGHT)
|
||||
R.drawable.geode_base_light else R.drawable.geode_base
|
||||
),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp, 64.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
stringResource(R.string.launcher_title),
|
||||
style = launcherTitleStyle,
|
||||
fontSize = 64.sp,
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (beginLaunch) {
|
||||
UpdateWarning(launchInSafeMode) {
|
||||
beginLaunch = false
|
||||
@Composable
|
||||
fun GeodeUpdateIndicator(snackbarHostState: SnackbarHostState, onRetry: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val resources = LocalResources.current
|
||||
|
||||
val releaseState = ReleaseManager.get(context).uiState.collectAsState().value
|
||||
|
||||
if (releaseState !is ReleaseManager.ReleaseManagerState.Failure) {
|
||||
return
|
||||
}
|
||||
|
||||
val message = when (releaseState.exception) {
|
||||
is UnknownHostException, is ConnectException ->
|
||||
stringResource(R.string.release_fetch_no_internet_short)
|
||||
is ReleaseManager.UpdateException -> {
|
||||
when (releaseState.exception.reason) {
|
||||
ReleaseManager.UpdateException.Reason.EXTERNAL_FILE_IN_USE -> stringResource(
|
||||
R.string.release_fetch_external_in_use_short
|
||||
)
|
||||
else -> stringResource(R.string.release_fetch_generic_short)
|
||||
}
|
||||
}
|
||||
is CancellationException -> null
|
||||
else -> stringResource(R.string.release_fetch_generic_short)
|
||||
}
|
||||
|
||||
if (message == null) {
|
||||
return
|
||||
}
|
||||
|
||||
LaunchedEffect(message) {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
actionLabel = resources.getString(R.string.release_fetch_button_retry),
|
||||
duration = SnackbarDuration.Indefinite,
|
||||
withDismissAction = true
|
||||
)
|
||||
|
||||
when (result) {
|
||||
SnackbarResult.ActionPerformed -> {
|
||||
onRetry()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AltMainScreen(
|
||||
launchViewModel: LaunchViewModel = viewModel(factory = LaunchViewModel.Factory)
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launchViewModel.beginLaunchFlow()
|
||||
}
|
||||
|
||||
val launchUIState by launchViewModel.uiState.collectAsState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var launchInSafeMode by remember {
|
||||
mutableStateOf(launchViewModel.launchArguments?.autoSafeMode == true)
|
||||
}
|
||||
|
||||
var showErrorInfo by remember { mutableStateOf(false) }
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
modifier = Modifier.safeDrawingPadding()
|
||||
) { innerPadding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
GeodeLogo(shouldAnimate = launchUIState.isInProgress())
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
LaunchProgressCard(
|
||||
launchUIState,
|
||||
crashInfo = launchViewModel.currentCrashInfo(),
|
||||
onCancel = {
|
||||
launchInSafeMode = false
|
||||
coroutineScope.launch {
|
||||
launchViewModel.cancelLaunch()
|
||||
}
|
||||
},
|
||||
onResume = { safeMode ->
|
||||
launchInSafeMode = safeMode
|
||||
|
||||
launchViewModel.clearCrashInfo()
|
||||
coroutineScope.launch {
|
||||
launchViewModel.beginLaunchFlow(true)
|
||||
}
|
||||
},
|
||||
onMore = {
|
||||
showErrorInfo = true
|
||||
},
|
||||
extraOptions = {
|
||||
ExtraOptions(
|
||||
onSettings = {
|
||||
coroutineScope.launch {
|
||||
launchViewModel.cancelLaunch(true)
|
||||
}
|
||||
|
||||
onSettings(context)
|
||||
}
|
||||
)
|
||||
},
|
||||
safeModeEnabled = launchInSafeMode
|
||||
)
|
||||
|
||||
// only show launcher update in a case where a user won't see it ingame
|
||||
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()
|
||||
}
|
||||
|
||||
GeodeUpdateIndicator(snackbarHostState) {
|
||||
launchViewModel.retryUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showSystemUi = true)
|
||||
@Composable
|
||||
fun MainScreenLightPreview() {
|
||||
GeodeLauncherTheme {
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||
MainScreen()
|
||||
if (launchUIState is LaunchViewModel.LaunchUIState.Ready) {
|
||||
UpdateWarning(launchInSafeMode) {
|
||||
launchInSafeMode = false
|
||||
coroutineScope.launch {
|
||||
launchViewModel.cancelLaunch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview(showSystemUi = true)
|
||||
@Composable
|
||||
fun MainScreenDarkPreview() {
|
||||
GeodeLauncherTheme(theme = Theme.DARK) {
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||
MainScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun MainScreenNoGeometryDashPreview() {
|
||||
GeodeLauncherTheme {
|
||||
MainScreen(gdInstalled = false)
|
||||
val loadFailure = launchViewModel.currentCrashInfo()
|
||||
if (showErrorInfo && loadFailure != null) {
|
||||
ErrorInfoSheet(loadFailure, onDismiss = { showErrorInfo = false })
|
||||
}
|
||||
}
|
||||
@@ -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,13 +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()
|
||||
@@ -115,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,
|
||||
@@ -134,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>
|
||||
@@ -200,6 +202,7 @@ data class LogLine(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun showException(exception: Exception) = LogLine(
|
||||
process = ProcessInformation(0, 0, 0),
|
||||
time = Clock.System.now(),
|
||||
@@ -210,12 +213,14 @@ data class LogLine(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
val identifier = time.toJavaInstant()
|
||||
|
||||
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}"
|
||||
|
||||
@@ -3,16 +3,13 @@ package com.geode.launcher.main
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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
|
||||
@@ -27,12 +24,13 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geode.launcher.R
|
||||
import com.geode.launcher.utils.Constants
|
||||
import androidx.core.net.toUri
|
||||
|
||||
fun onDownloadGame(context: Context) {
|
||||
val appUrl = "https://play.google.com/store/apps/details?id=${Constants.PACKAGE_NAME}"
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse(appUrl)
|
||||
data = appUrl.toUri()
|
||||
setPackage("com.android.vending")
|
||||
}
|
||||
context.startActivity(intent)
|
||||
@@ -55,22 +53,6 @@ fun GooglePlayBadge(modifier: Modifier = Modifier) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatusIndicator(
|
||||
icon: @Composable () -> Unit,
|
||||
text: @Composable () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
ElevatedCard(modifier = modifier, onClick = onClick) {
|
||||
Row(modifier = Modifier.padding(8.dp)) {
|
||||
icon()
|
||||
Spacer(Modifier.size(8.dp))
|
||||
text()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InlineDialog(headline: @Composable () -> Unit, body: @Composable () -> Unit, modifier: Modifier = Modifier) {
|
||||
ElevatedCard(modifier = modifier) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.geode.launcher.main
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
@@ -18,8 +19,6 @@ 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.Share
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -34,10 +33,14 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
@@ -57,6 +60,7 @@ import com.geode.launcher.ui.theme.Typography
|
||||
import com.geode.launcher.utils.GamePackageUtils
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class LoadFailureInfo(
|
||||
val title: LaunchUtils.LauncherError,
|
||||
@@ -209,9 +213,12 @@ fun ErrorInfoDescription(description: String, modifier: Modifier = Modifier) {
|
||||
@Composable
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val clipboard = LocalClipboard.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val resources = LocalResources.current
|
||||
|
||||
FlowRow(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
@@ -229,7 +236,15 @@ fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
|
||||
}
|
||||
|
||||
FilledTonalButton(onClick = {
|
||||
clipboardManager.setText(AnnotatedString(extraDetails))
|
||||
val clipboardLabel = resources.getString(R.string.application_log_copy_label)
|
||||
|
||||
coroutineScope.launch {
|
||||
clipboard.setClipEntry(
|
||||
ClipEntry(ClipData.newPlainText(
|
||||
clipboardLabel, extraDetails
|
||||
))
|
||||
)
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_content_copy),
|
||||
@@ -241,13 +256,17 @@ fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
|
||||
} else {
|
||||
Button(onClick = { onShareCrash(context) }) {
|
||||
Icon(
|
||||
Icons.Filled.Share,
|
||||
painterResource(R.drawable.icon_share),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_error_export_crash))
|
||||
}
|
||||
|
||||
val developerSettingsEnabled = remember {
|
||||
PreferenceUtils.get(context).getBoolean(PreferenceUtils.Key.DEVELOPER_MODE)
|
||||
}
|
||||
if (developerSettingsEnabled) {
|
||||
FilledTonalButton(onClick = { onShowLogs(context) }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_description),
|
||||
@@ -259,6 +278,7 @@ fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -4,21 +4,12 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.PressInteraction
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -28,25 +19,21 @@ 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.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
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 com.geode.launcher.GeometryDashActivity
|
||||
import com.geode.launcher.R
|
||||
import com.geode.launcher.preferences.SettingsActivity
|
||||
import com.geode.launcher.ui.theme.Typography
|
||||
import com.geode.launcher.utils.Constants
|
||||
import com.geode.launcher.utils.GamePackageUtils
|
||||
import com.geode.launcher.utils.GeodeUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import com.geode.launcher.utils.useCountdownTimer
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
||||
@@ -71,7 +58,7 @@ fun UpdateWarning(inSafeMode: Boolean = false, onDismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Filled.Warning,
|
||||
painterResource(R.drawable.icon_warning),
|
||||
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
|
||||
)
|
||||
},
|
||||
@@ -116,11 +103,11 @@ fun SafeModeDialog(onDismiss: () -> Unit, onLaunch: () -> Unit) {
|
||||
AlertDialog(
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Filled.Warning,
|
||||
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
|
||||
painterResource(R.drawable.icon_shield_alert),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
title = { Text(stringResource(R.string.safe_mode_enable_title)) },
|
||||
title = { Text(stringResource(R.string.preference_launch_safe_mode_confirm_title)) },
|
||||
text = { Text(stringResource(R.string.safe_mode_enable_description)) },
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
@@ -197,105 +184,12 @@ fun LongPressButton(onClick: () -> Unit, onLongPress: () -> Unit, enabled: Boole
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlayButton(
|
||||
stopAutomaticLaunch: Boolean,
|
||||
blockLaunch: Boolean,
|
||||
onPlayGame: (Boolean) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var showSafeModeDialog by remember { mutableStateOf(false) }
|
||||
var launchInSafeMode by remember { mutableStateOf(false) }
|
||||
|
||||
val shouldAutomaticallyLaunch by PreferenceUtils.useBooleanPreference(
|
||||
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
|
||||
)
|
||||
|
||||
if (shouldAutomaticallyLaunch && !stopAutomaticLaunch && !showSafeModeDialog) {
|
||||
val countdownTimer = useCountdownTimer(
|
||||
time = 3000,
|
||||
onCountdownFinish = { onPlayGame(launchInSafeMode) }
|
||||
)
|
||||
|
||||
if (countdownTimer != 0L) {
|
||||
Text(
|
||||
pluralStringResource(
|
||||
R.plurals.automatically_load_countdown,
|
||||
countdownTimer.toInt(),
|
||||
countdownTimer
|
||||
),
|
||||
style = Typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
)
|
||||
Spacer(Modifier.size(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
LongPressButton(
|
||||
enabled = !blockLaunch,
|
||||
onLongPress = {
|
||||
showSafeModeDialog = true
|
||||
},
|
||||
onClick = {
|
||||
onPlayGame(launchInSafeMode)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.PlayArrow,
|
||||
contentDescription = context.getString(R.string.launcher_launch_icon_alt)
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(context.getString(R.string.launcher_launch))
|
||||
}
|
||||
Spacer(Modifier.size(2.dp))
|
||||
IconButton(onClick = { onSettings(context) }) {
|
||||
Icon(
|
||||
Icons.Filled.Settings,
|
||||
contentDescription = context.getString(R.string.launcher_settings_icon_alt)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showSafeModeDialog) {
|
||||
SafeModeDialog(
|
||||
onDismiss = {
|
||||
launchInSafeMode = false
|
||||
showSafeModeDialog = false
|
||||
},
|
||||
onLaunch = {
|
||||
launchInSafeMode = true
|
||||
|
||||
onPlayGame(true)
|
||||
|
||||
showSafeModeDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LaunchBlockedLabel(text: String) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val showDownload = remember { GamePackageUtils.showDownloadBadge(context.packageManager) }
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(text, textAlign = TextAlign.Center, modifier = Modifier.padding(12.dp))
|
||||
|
||||
if (showDownload) {
|
||||
GooglePlayBadge()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UnsupportedVersionWarning(modifier: Modifier = Modifier) {
|
||||
InlineDialog(
|
||||
headline = {
|
||||
Icon(
|
||||
Icons.Default.Warning,
|
||||
painterResource(R.drawable.icon_warning),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
|
||||
@@ -13,9 +13,6 @@ import androidx.compose.foundation.layout.displayCutoutPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
@@ -41,6 +38,7 @@ 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.GeodeUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@@ -127,7 +125,9 @@ fun LaunchNotification() {
|
||||
// surface is not in use, so this is unfortunately not provided
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
||||
Column(
|
||||
modifier = Modifier.displayCutoutPadding()
|
||||
modifier = if (GeodeUtils.handleSafeArea)
|
||||
Modifier.displayCutoutPadding()
|
||||
else Modifier
|
||||
) {
|
||||
cards.forEach {
|
||||
NotificationCardFromType(it)
|
||||
@@ -199,7 +199,7 @@ fun LauncherUpdateContent(modifier: Modifier = Modifier) {
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.Info,
|
||||
painterResource(R.drawable.icon_info),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
@@ -232,7 +232,7 @@ fun OutdatedVersionContent(modifier: Modifier = Modifier) {
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.Warning,
|
||||
painterResource(R.drawable.icon_warning),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
@@ -251,7 +251,7 @@ fun UpdateFailedContent(modifier: Modifier = Modifier) {
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.Warning,
|
||||
painterResource(R.drawable.icon_warning),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -18,7 +18,11 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val COOLDOWN_LENGTH_MS = 3000L
|
||||
data class LaunchArguments(
|
||||
val autoSafeMode: Boolean,
|
||||
val forcePause: Boolean,
|
||||
val forceLaunch: Boolean
|
||||
)
|
||||
|
||||
class LaunchViewModel(private val application: Application): ViewModel() {
|
||||
companion object {
|
||||
@@ -37,7 +41,13 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
||||
private var readyTimer: CountDownTimer? = null
|
||||
|
||||
private fun initReadyTimer() {
|
||||
readyTimer = object : CountDownTimer(COOLDOWN_LENGTH_MS, 1000) {
|
||||
val cooldownLength = PreferenceUtils.get(application).getInt(PreferenceUtils.Key.WAIT_PERIOD) * 1000L
|
||||
if (cooldownLength == 0L) {
|
||||
readyTimerPassed = true
|
||||
return
|
||||
}
|
||||
|
||||
readyTimer = object : CountDownTimer(cooldownLength, 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
// no tick necessary
|
||||
}
|
||||
@@ -80,18 +90,43 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
||||
data class Cancelled(val reason: LaunchCancelReason, val inProgress: Boolean = false) : LaunchUIState()
|
||||
data object Working : LaunchUIState()
|
||||
data object Ready : LaunchUIState()
|
||||
|
||||
fun isInProgress() = when (this) {
|
||||
is Ready,
|
||||
is Working,
|
||||
is Updating,
|
||||
is UpdateCheck -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private val _uiState: MutableStateFlow<LaunchUIState>
|
||||
|
||||
init {
|
||||
val preferenceUtils = PreferenceUtils.get(application)
|
||||
val initialCancel = !preferenceUtils.getBoolean(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
|
||||
&& !preferenceUtils.getBoolean(PreferenceUtils.Key.LOAD_AUTOMATICALLY)
|
||||
|
||||
// fixes a single frame of progress being displayed when we're not actually going to do work
|
||||
_uiState = MutableStateFlow<LaunchUIState>(
|
||||
if (initialCancel) LaunchUIState.Cancelled(LaunchCancelReason.AUTOMATIC)
|
||||
else LaunchUIState.Initial
|
||||
)
|
||||
}
|
||||
|
||||
private val _uiState = MutableStateFlow<LaunchUIState>(LaunchUIState.Initial)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
val nextLauncherUpdate = ReleaseManager.get(application).availableLauncherUpdate
|
||||
|
||||
var loadFailure: LoadFailureInfo? = null
|
||||
var launchArguments: LaunchArguments? = null
|
||||
|
||||
private var hasCheckedForUpdates = false
|
||||
private var isCancelling = false
|
||||
private var hasManuallyStarted = false
|
||||
|
||||
private var restoreUpdateState: LaunchUIState? = null
|
||||
|
||||
private suspend fun determineUpdateStatus() {
|
||||
ReleaseManager.get(application).uiState.takeWhile {
|
||||
it is ReleaseManager.ReleaseManagerState.InUpdateCheck || it is ReleaseManager.ReleaseManagerState.InDownload
|
||||
@@ -106,6 +141,13 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
||||
|
||||
// flow terminates once update check is finished
|
||||
hasCheckedForUpdates = true
|
||||
|
||||
val currentState = restoreUpdateState
|
||||
if (currentState != null) {
|
||||
_uiState.emit(currentState)
|
||||
return
|
||||
}
|
||||
|
||||
preReadyCheck()
|
||||
}
|
||||
|
||||
@@ -157,8 +199,15 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
||||
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GEODE_NOT_FOUND))
|
||||
}
|
||||
|
||||
// if forcing immediate launch, then act as if it's manually started (skips timers)
|
||||
val forceImmediate = launchArguments?.forceLaunch == true
|
||||
if (forceImmediate) {
|
||||
hasManuallyStarted = true
|
||||
}
|
||||
|
||||
val loadAutomatically = PreferenceUtils.get(application).getBoolean(PreferenceUtils.Key.LOAD_AUTOMATICALLY)
|
||||
if (!loadAutomatically && !hasManuallyStarted) {
|
||||
val forcePause = launchArguments?.forcePause == true
|
||||
if (!hasManuallyStarted && (forcePause || !loadAutomatically)) {
|
||||
return cancelLaunch(true)
|
||||
}
|
||||
|
||||
@@ -201,6 +250,17 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
||||
preReadyCheck()
|
||||
}
|
||||
|
||||
fun retryUpdate() {
|
||||
ReleaseManager.get(application).checkForUpdates(true)
|
||||
if (ReleaseManager.get(application).isInUpdate) {
|
||||
restoreUpdateState = _uiState.value
|
||||
viewModelScope.launch {
|
||||
determineUpdateStatus()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun cancelLaunch(isAutomatic: Boolean = false) {
|
||||
// no need to double cancel
|
||||
if (_uiState.value is LaunchUIState.Cancelled) {
|
||||
@@ -210,10 +270,10 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
||||
val reason = if (isAutomatic) LaunchCancelReason.AUTOMATIC else LaunchCancelReason.MANUAL
|
||||
|
||||
isCancelling = true
|
||||
_uiState.emit(LaunchUIState.Cancelled(reason, true))
|
||||
|
||||
val releaseManager = ReleaseManager.get(application)
|
||||
if (releaseManager.isInUpdate) {
|
||||
_uiState.emit(LaunchUIState.Cancelled(reason, true))
|
||||
releaseManager.cancelUpdate()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
package com.geode.launcher.main
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import android.text.format.Formatter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
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.shape.RoundedCornerShape
|
||||
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.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
@@ -35,10 +35,12 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -46,26 +48,30 @@ 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.style.TextAlign
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.geode.launcher.InstallReceiver
|
||||
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.markdownColor
|
||||
import com.mikepenz.markdown.m3.markdownTypography
|
||||
import kotlinx.datetime.toJavaInstant
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
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.toJavaInstant
|
||||
|
||||
fun clearDownloadedApks(context: Context) {
|
||||
// technically we should be using the activity results but it was too inconsistent for my liking
|
||||
@@ -84,57 +90,172 @@ fun clearDownloadedApks(context: Context) {
|
||||
it.delete()
|
||||
}
|
||||
|
||||
context.filesDir.listFiles { _, name -> name.lowercase().endsWith(".apk") }?.forEach {
|
||||
it.delete()
|
||||
}
|
||||
|
||||
preferenceUtils.setBoolean(PreferenceUtils.Key.CLEANUP_APKS, false)
|
||||
}
|
||||
|
||||
fun generateInstallIntent(uri: Uri): Intent {
|
||||
// maybe one day i'll rewrite this to use packageinstaller. not today
|
||||
return Intent(Intent.ACTION_INSTALL_PACKAGE).apply {
|
||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
// lazy singleton idc
|
||||
object LauncherUpdater {
|
||||
enum class LauncherUpdateState {
|
||||
Began,
|
||||
Active,
|
||||
Inactive,
|
||||
Finished;
|
||||
}
|
||||
|
||||
private val _uiState = MutableStateFlow<LauncherUpdateState>(LauncherUpdateState.Inactive)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
suspend fun resetState() {
|
||||
_uiState.emit(LauncherUpdateState.Inactive)
|
||||
}
|
||||
|
||||
suspend fun installPackage(context: Context, file: File) {
|
||||
if (!file.exists()) {
|
||||
_uiState.emit(LauncherUpdateState.Finished)
|
||||
return
|
||||
}
|
||||
|
||||
_uiState.emit(LauncherUpdateState.Began)
|
||||
|
||||
val installer = context.packageManager.packageInstaller
|
||||
|
||||
installer.registerSessionCallback(object : PackageInstaller.SessionCallback() {
|
||||
override fun onActiveChanged(sessionId: Int, active: Boolean) {
|
||||
if (active) {
|
||||
_uiState.tryEmit(LauncherUpdateState.Active)
|
||||
} else {
|
||||
_uiState.tryEmit(LauncherUpdateState.Began)
|
||||
}
|
||||
}
|
||||
|
||||
fun installLauncherUpdate(context: Context) {
|
||||
override fun onBadgingChanged(sessionId: Int) {}
|
||||
|
||||
override fun onCreated(sessionId: Int) {
|
||||
_uiState.tryEmit(LauncherUpdateState.Began)
|
||||
}
|
||||
|
||||
override fun onFinished(sessionId: Int, success: Boolean) {
|
||||
_uiState.tryEmit(LauncherUpdateState.Finished)
|
||||
}
|
||||
|
||||
override fun onProgressChanged(sessionId: Int, progress: Float) {
|
||||
_uiState.tryEmit(LauncherUpdateState.Active)
|
||||
}
|
||||
})
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
file.inputStream().use { apkStream ->
|
||||
val length = file.length()
|
||||
|
||||
val params = PackageInstaller.SessionParams(
|
||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL,
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
|
||||
}
|
||||
|
||||
val sessionId = installer.createSession(params)
|
||||
val session = installer.openSession(sessionId)
|
||||
|
||||
session.openWrite("package", 0, length).use { sessionStream ->
|
||||
apkStream.copyTo(sessionStream)
|
||||
session.fsync(sessionStream)
|
||||
}
|
||||
|
||||
val intent = Intent(context, InstallReceiver::class.java)
|
||||
val pi = PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
||||
)
|
||||
|
||||
session.commit(pi.intentSender)
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun installLauncherUpdate(context: Context) {
|
||||
val nextUpdate = ReleaseManager.get(context).availableLauncherUpdate.value
|
||||
val launcherDownload = nextUpdate?.getDownload()
|
||||
|
||||
if (launcherDownload != null) {
|
||||
val outputFile = launcherDownload.filename
|
||||
val baseDirectory = LaunchUtils.getBaseDirectory(context)
|
||||
val baseDirectory = context.filesDir
|
||||
|
||||
val outputPathFile = File(baseDirectory, outputFile)
|
||||
if (!outputPathFile.exists()) {
|
||||
return
|
||||
}
|
||||
|
||||
val outputPath = "${UserDirectoryProvider.ROOT}${outputFile}"
|
||||
|
||||
val uri = DocumentsContract.buildDocumentUri("${context.packageName}.user", outputPath)
|
||||
|
||||
PreferenceUtils.get(context).setBoolean(PreferenceUtils.Key.CLEANUP_APKS, true)
|
||||
|
||||
try {
|
||||
val intent = generateInstallIntent(uri)
|
||||
context.startActivity(intent)
|
||||
} 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)
|
||||
}
|
||||
LauncherUpdater.installPackage(context, outputPathFile)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
val msg = context.getString(R.string.launcher_self_update_failed, e.message)
|
||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
||||
|
||||
context.startActivity(downloadIntent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(context, context.getString(R.string.no_activity_found), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
LauncherUpdater.resetState()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, context.getString(R.string.release_fetch_no_releases), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LauncherUpdateDialog(onDismiss: () -> Unit) {
|
||||
val updateState by LauncherUpdater.uiState.collectAsState()
|
||||
|
||||
val cancellable = updateState != LauncherUpdater.LauncherUpdateState.Active
|
||||
|
||||
LaunchedEffect(updateState) {
|
||||
if (updateState == LauncherUpdater.LauncherUpdateState.Finished) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
if (cancellable) {
|
||||
return
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
if (cancellable) onDismiss()
|
||||
},
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(18.dp),
|
||||
modifier = Modifier.padding(24.dp)
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
|
||||
Text(
|
||||
stringResource(R.string.launcher_self_update_progress),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
||||
@@ -142,6 +263,14 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
||||
|
||||
val nextRelease = remember { ReleaseManager.get(context).availableLauncherUpdate.value }
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
var beginUpdate by remember { mutableStateOf(false) }
|
||||
if (beginUpdate) {
|
||||
LauncherUpdateDialog(onDismiss = {
|
||||
beginUpdate = false
|
||||
})
|
||||
}
|
||||
|
||||
if (nextRelease != null) {
|
||||
ModalBottomSheet(onDismissRequest = { onDismiss() }, sheetState = sheetState) {
|
||||
@@ -165,7 +294,12 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
||||
|
||||
Row(modifier = Modifier.padding(vertical = 12.dp)) {
|
||||
Button(
|
||||
onClick = { installLauncherUpdate(context) },
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
installLauncherUpdate(context)
|
||||
beginUpdate = true
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(painterResource(R.drawable.icon_download), contentDescription = null)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
@@ -176,7 +310,7 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
||||
|
||||
IconButton(onClick = { uriHandler.openUri(nextRelease.release.htmlUrl) }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.xps_icon),
|
||||
painterResource(R.drawable.icon_link),
|
||||
stringResource(R.string.launcher_update_external_link)
|
||||
)
|
||||
}
|
||||
@@ -191,17 +325,19 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
||||
style = Typography.titleLarge
|
||||
)
|
||||
|
||||
CompositionLocalProvider(LocalBulletListHandler provides { _, _, _ -> "• " }) {
|
||||
// every new release of the markdown library adds another arg to it
|
||||
CompositionLocalProvider(LocalBulletListHandler provides { _, _, _, _, _ -> "• " }) {
|
||||
Markdown(
|
||||
content = nextRelease.release.body.replace("\r", ""),
|
||||
colors = markdownColor(
|
||||
linkText = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
typography = markdownTypography(
|
||||
link = MaterialTheme.typography.bodyLarge.copy(
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
textLink = TextLinkStyles(
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
).toSpanStyle()
|
||||
),
|
||||
),
|
||||
flavour = GFMFlavourDescriptor(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -216,6 +352,7 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
||||
@Composable
|
||||
fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
var showInfoDialog by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -225,6 +362,13 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
|
||||
}
|
||||
}
|
||||
|
||||
var beginUpdate by remember { mutableStateOf(false) }
|
||||
if (beginUpdate) {
|
||||
LauncherUpdateDialog {
|
||||
beginUpdate = false
|
||||
}
|
||||
}
|
||||
|
||||
ElevatedCard(modifier) {
|
||||
Column(
|
||||
// buttons add enough padding already, lower to compensate
|
||||
@@ -249,7 +393,12 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = { installLauncherUpdate(context) },
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
installLauncherUpdate(context)
|
||||
beginUpdate = true
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.launcher_install))
|
||||
}
|
||||
@@ -257,171 +406,3 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateProgressIndicator(
|
||||
message: String,
|
||||
releaseViewModel: ReleaseViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
progress: (() -> Float)? = null
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(message, style = MaterialTheme.typography.labelMedium)
|
||||
|
||||
Spacer(modifier = Modifier.padding(4.dp))
|
||||
|
||||
if (progress == null) {
|
||||
LinearProgressIndicator()
|
||||
} else {
|
||||
LinearProgressIndicator(progress = progress)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
releaseViewModel.cancelUpdate()
|
||||
},
|
||||
modifier = Modifier.offset((-12).dp)
|
||||
) {
|
||||
Text(stringResource(R.string.release_fetch_button_cancel))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateMessageIndicator(
|
||||
message: String,
|
||||
releaseViewModel: ReleaseViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
allowRetry: Boolean = false
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(
|
||||
message,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.padding(4.dp))
|
||||
|
||||
if (allowRetry) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
releaseViewModel.runReleaseCheck(true)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Refresh,
|
||||
contentDescription = stringResource(R.string.launcher_retry_icon_alt)
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.release_fetch_button_retry))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val releaseState by releaseViewModel.uiState.collectAsState()
|
||||
|
||||
when (val state = releaseState) {
|
||||
is ReleaseViewModel.ReleaseUIState.Failure -> {
|
||||
val message = when (state.exception) {
|
||||
is UnknownHostException, is ConnectException ->
|
||||
stringResource(R.string.release_fetch_no_internet)
|
||||
is ReleaseManager.UpdateException -> {
|
||||
when (state.exception.reason) {
|
||||
ReleaseManager.UpdateException.Reason.EXTERNAL_FILE_IN_USE -> stringResource(
|
||||
R.string.release_fetch_manual_check_required
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
UpdateMessageIndicator(
|
||||
stringResource(
|
||||
R.string.release_fetch_failed,
|
||||
message ?: stringResource(R.string.release_fetch_try_later)
|
||||
),
|
||||
modifier = modifier,
|
||||
allowRetry = true,
|
||||
releaseViewModel = releaseViewModel
|
||||
)
|
||||
}
|
||||
is ReleaseViewModel.ReleaseUIState.InDownload -> {
|
||||
val downloaded = remember(state.downloaded) {
|
||||
Formatter.formatShortFileSize(context, state.downloaded)
|
||||
}
|
||||
|
||||
val outOf = remember(state.outOf) {
|
||||
state.outOf?.run {
|
||||
Formatter.formatShortFileSize(context, this)
|
||||
}
|
||||
}
|
||||
|
||||
UpdateProgressIndicator(
|
||||
stringResource(
|
||||
R.string.release_fetch_downloading,
|
||||
downloaded,
|
||||
outOf ?: "…"
|
||||
),
|
||||
modifier = modifier,
|
||||
releaseViewModel = releaseViewModel,
|
||||
progress = {
|
||||
val progress = state.downloaded / state.outOf!!.toDouble()
|
||||
progress.toFloat()
|
||||
}.takeIf { state.outOf != null }
|
||||
)
|
||||
}
|
||||
is ReleaseViewModel.ReleaseUIState.InUpdateCheck -> {
|
||||
UpdateProgressIndicator(
|
||||
stringResource(R.string.release_fetch_in_progress),
|
||||
modifier = modifier,
|
||||
releaseViewModel = releaseViewModel
|
||||
)
|
||||
}
|
||||
is ReleaseViewModel.ReleaseUIState.Finished -> {
|
||||
if (state.hasUpdated) {
|
||||
UpdateMessageIndicator(
|
||||
stringResource(R.string.release_fetch_success),
|
||||
modifier = modifier,
|
||||
releaseViewModel = releaseViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
is ReleaseViewModel.ReleaseUIState.Cancelled -> {
|
||||
if (state.isCancelling) {
|
||||
UpdateProgressIndicator(
|
||||
stringResource(R.string.release_fetch_cancelling),
|
||||
modifier = modifier,
|
||||
releaseViewModel = releaseViewModel
|
||||
)
|
||||
} else {
|
||||
UpdateMessageIndicator(
|
||||
stringResource(R.string.release_fetch_cancelled),
|
||||
modifier = modifier,
|
||||
allowRetry = true,
|
||||
releaseViewModel = releaseViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val nextUpdate by releaseViewModel.nextLauncherUpdate.collectAsState()
|
||||
|
||||
if (!releaseViewModel.isInUpdate && nextUpdate != null) {
|
||||
LauncherUpdateIndicator(
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
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.rememberScrollState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
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.TextButton
|
||||
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.painterResource
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IconSelectConfirmDialog(selectedIcon: ApplicationIconDetails, onDismissRequest: () -> Unit) {
|
||||
AlertDialog(
|
||||
icon = {
|
||||
Image(
|
||||
adaptiveIconPainterResource(selectedIcon.iconId),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(
|
||||
R.string.application_icon_confirm,
|
||||
stringResource(selectedIcon.nameId)
|
||||
))
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.application_icon_confirm_description))
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.message_box_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
val context = LocalContext.current
|
||||
|
||||
TextButton(onClick = { onSelectIcon(context, selectedIcon) }) {
|
||||
Text(stringResource(R.string.message_box_accept))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
}
|
||||
|
||||
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) {
|
||||
var showSelection by remember { mutableStateOf(false) }
|
||||
|
||||
if (showSelection) {
|
||||
IconSelectConfirmDialog(iconData) {
|
||||
showSelection = false
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 1.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.then(modifier)
|
||||
.selectable(
|
||||
selected = selected,
|
||||
onClick = {
|
||||
if (!selected)
|
||||
showSelection = true
|
||||
},
|
||||
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()
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
Column {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_arrow_back),
|
||||
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()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.geode.launcher.preferences
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.ClipData
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
@@ -18,11 +18,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
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
|
||||
@@ -50,13 +45,13 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
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
|
||||
@@ -70,6 +65,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 +79,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
|
||||
@@ -148,7 +145,10 @@ fun SelectLogLevelDialog(
|
||||
@Composable
|
||||
fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
|
||||
val haptics = LocalHapticFeedback.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val clipboard = LocalClipboard.current
|
||||
val resources = LocalResources.current
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
@@ -157,7 +157,17 @@ fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
|
||||
detectTapGestures(
|
||||
onLongPress = {
|
||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
clipboardManager.setText(AnnotatedString(logLine.asSimpleString))
|
||||
|
||||
val clipboardLabel = resources.getString(R.string.application_log_copy_label)
|
||||
val clipboardData = logLine.asSimpleString
|
||||
|
||||
coroutineScope.launch {
|
||||
clipboard.setClipEntry(
|
||||
ClipEntry(ClipData.newPlainText(
|
||||
clipboardLabel, clipboardData
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -166,12 +176,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
|
||||
)
|
||||
}
|
||||
@@ -187,6 +197,7 @@ fun ApplicationLogsScreen(
|
||||
val isLoading by logViewModel.isLoading.collectAsState()
|
||||
|
||||
val context = LocalContext.current
|
||||
val resources = LocalResources.current
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
@@ -234,7 +245,7 @@ fun ApplicationLogsScreen(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
painterResource(R.drawable.icon_arrow_back),
|
||||
contentDescription = stringResource(R.string.back_icon_alt)
|
||||
)
|
||||
}
|
||||
@@ -242,26 +253,17 @@ 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(resources.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 }) {
|
||||
Icon(
|
||||
Icons.Filled.MoreVert,
|
||||
painterResource(R.drawable.icon_more_vert),
|
||||
contentDescription = stringResource(R.string.application_logs_more)
|
||||
)
|
||||
}
|
||||
@@ -280,7 +282,7 @@ fun ApplicationLogsScreen(
|
||||
trailingIcon = {
|
||||
if (logViewModel.filterCrashes) {
|
||||
Icon(
|
||||
Icons.Filled.Check,
|
||||
painterResource(R.drawable.icon_check),
|
||||
contentDescription = stringResource(R.string.application_logs_crash_only_enabled_alt)
|
||||
)
|
||||
}
|
||||
@@ -309,23 +311,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,305 @@
|
||||
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.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.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(
|
||||
painterResource(R.drawable.icon_share),
|
||||
contentDescription = stringResource(R.string.application_logs_share)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
crashViewModel.removeCrash(crashDump.filename)
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_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(
|
||||
painterResource(R.drawable.icon_arrow_back),
|
||||
contentDescription = stringResource(R.string.back_icon_alt)
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_more_vert),
|
||||
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,24 +1,26 @@
|
||||
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
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
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.MaterialTheme
|
||||
@@ -32,6 +34,9 @@ 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.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -39,7 +44,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() {
|
||||
@@ -52,9 +57,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
|
||||
@@ -67,6 +73,30 @@ class DeveloperSettingsActivity : 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 onOpenLogs(context: Context) {
|
||||
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
@@ -80,7 +110,7 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
painterResource(R.drawable.icon_arrow_back),
|
||||
contentDescription = stringResource(R.string.back_icon_alt)
|
||||
)
|
||||
}
|
||||
@@ -93,6 +123,7 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.background),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -107,16 +138,12 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
|
||||
val developerMode by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
|
||||
|
||||
OptionsGroup {
|
||||
SettingsCard(
|
||||
title = stringResource(R.string.preference_developer_mode),
|
||||
preferenceKey = PreferenceUtils.Key.DEVELOPER_MODE,
|
||||
)
|
||||
|
||||
InlineText(
|
||||
stringResource(R.string.preference_developer_mode_about)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
if (developerMode) {
|
||||
OptionsGroup(title = stringResource(R.string.preference_category_gameplay)) {
|
||||
@@ -130,13 +157,14 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
}}
|
||||
)
|
||||
|
||||
/*
|
||||
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),
|
||||
@@ -145,6 +173,26 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
*/
|
||||
}
|
||||
|
||||
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),
|
||||
@@ -156,23 +204,27 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
2 to stringResource(R.string.preference_release_channel_nightly),
|
||||
)
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OptionsGroup(title = stringResource(R.string.preference_category_testing)) {
|
||||
SettingsCard(
|
||||
title = stringResource(R.string.preference_enable_redesign),
|
||||
preferenceKey = PreferenceUtils.Key.ENABLE_REDESIGN,
|
||||
SettingsSelectCard(
|
||||
title = stringResource(R.string.preference_launch_delay),
|
||||
dialogTitle = stringResource(R.string.preference_launch_delay_select),
|
||||
preferenceKey = PreferenceUtils.Key.WAIT_PERIOD,
|
||||
options = linkedMapOf(
|
||||
0 to stringResource(R.string.preference_launch_delay_instant),
|
||||
1 to pluralStringResource(R.plurals.preference_launch_delay_value, 1, 1),
|
||||
3 to pluralStringResource(R.plurals.preference_launch_delay_value, 3, 3),
|
||||
5 to pluralStringResource(R.plurals.preference_launch_delay_value, 5, 5),
|
||||
)
|
||||
)
|
||||
|
||||
/*
|
||||
SettingsCard(
|
||||
title = stringResource(R.string.preference_disable_redesign),
|
||||
preferenceKey = PreferenceUtils.Key.DISABLE_REDESIGN,
|
||||
)
|
||||
*/
|
||||
}
|
||||
|
||||
OptionsGroup(title = stringResource(R.string.preference_profiles)) {
|
||||
@@ -180,6 +232,8 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
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.text.format.DateUtils
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
@@ -23,43 +22,46 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.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
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.geode.launcher.BuildConfig
|
||||
import com.geode.launcher.MainActivity
|
||||
import com.geode.launcher.R
|
||||
import com.geode.launcher.UserDirectoryProvider
|
||||
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.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
|
||||
import kotlin.time.Clock
|
||||
|
||||
class SettingsActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -71,9 +73,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
|
||||
@@ -88,25 +91,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
|
||||
@@ -127,7 +111,7 @@ fun onOpenFileManager(context: Context) {
|
||||
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getText(R.string.no_activity_found), Toast.LENGTH_SHORT
|
||||
@@ -141,6 +125,7 @@ fun UpdateIndicator(
|
||||
updateStatus: ReleaseViewModel.ReleaseUIState
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val resources = LocalResources.current
|
||||
|
||||
var enablePopup by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -176,8 +161,8 @@ fun UpdateIndicator(
|
||||
is ReleaseViewModel.ReleaseUIState.Failure -> {
|
||||
val message = when (updateStatus.exception) {
|
||||
is UnknownHostException, is ConnectException ->
|
||||
context.getString(R.string.release_fetch_no_internet)
|
||||
else -> context.getString(R.string.preference_check_for_updates_failed)
|
||||
resources.getString(R.string.release_fetch_no_internet)
|
||||
else -> resources.getString(R.string.preference_check_for_updates_failed)
|
||||
}
|
||||
|
||||
snackbarHostState.showSnackbar(message)
|
||||
@@ -185,17 +170,17 @@ fun UpdateIndicator(
|
||||
is ReleaseViewModel.ReleaseUIState.Finished -> {
|
||||
if (updateStatus.hasUpdated) {
|
||||
snackbarHostState.showSnackbar(
|
||||
context.getString(R.string.preference_check_for_updates_success)
|
||||
resources.getString(R.string.preference_check_for_updates_success)
|
||||
)
|
||||
} else {
|
||||
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)
|
||||
resources.getString(R.string.launcher_game_update_required_short)
|
||||
)
|
||||
} else {
|
||||
snackbarHostState.showSnackbar(
|
||||
context.getString(R.string.preference_check_for_updates_none_found)
|
||||
resources.getString(R.string.preference_check_for_updates_none_found)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -226,82 +211,41 @@ 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)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
onBackPressedDispatcher: OnBackPressedDispatcher?,
|
||||
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
|
||||
) {
|
||||
fun DeveloperModeDialog(onDismiss: () -> Unit, onEnable: () -> Unit) {
|
||||
AlertDialog(
|
||||
icon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_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
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GeneralSettingsGroup() {
|
||||
val context = LocalContext.current
|
||||
|
||||
val currentRelease by PreferenceUtils.useStringPreference(PreferenceUtils.Key.CURRENT_VERSION_TAG)
|
||||
val updateStatus by releaseViewModel.uiState.collectAsState()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
var showUpdateInProgress by remember { mutableStateOf(false) }
|
||||
|
||||
if (showUpdateInProgress) {
|
||||
LaunchedEffect(snackbarHostState) {
|
||||
snackbarHostState.showSnackbar(
|
||||
context.getString(R.string.preference_check_for_updates_already_updating)
|
||||
)
|
||||
showUpdateInProgress = false
|
||||
}
|
||||
}
|
||||
|
||||
// fix theme transition by giving it the exact same animation as the top bar
|
||||
val containerColor by animateColorAsState(
|
||||
targetValue = MaterialTheme.colorScheme.background,
|
||||
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
|
||||
label = "background color"
|
||||
)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
containerColor = containerColor,
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = snackbarHostState)
|
||||
},
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = context.getString(R.string.back_icon_alt)
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
context.getString(R.string.title_activity_settings),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OptionsGroup(context.getString(R.string.preference_category_testing)) {
|
||||
OptionsGroup(stringResource(R.string.preference_category_testing)) {
|
||||
SettingsSelectCard(
|
||||
title = stringResource(R.string.preference_theme_name),
|
||||
dialogTitle = stringResource(R.string.preference_theme_select),
|
||||
@@ -317,16 +261,54 @@ fun SettingsScreen(
|
||||
title = stringResource(R.string.preference_black_background_name),
|
||||
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
|
||||
)
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preferences_open_file_manager),
|
||||
onClick = { onOpenFileManager(context) }
|
||||
|
||||
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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GameplaySettingsGroup() {
|
||||
val context = LocalContext.current
|
||||
|
||||
OptionsGroup(stringResource(R.string.preference_category_gameplay)) {
|
||||
SettingsCard(
|
||||
title = context.getString(R.string.preference_load_automatically_name),
|
||||
description = context.getString(R.string.preference_load_automatically_description),
|
||||
title = stringResource(R.string.preference_load_automatically_name),
|
||||
description = stringResource(R.string.preference_load_automatically_description),
|
||||
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY,
|
||||
)
|
||||
SettingsSelectCard(
|
||||
@@ -371,7 +353,7 @@ fun SettingsScreen(
|
||||
step = 5,
|
||||
) {
|
||||
SettingsCard(
|
||||
title = context.getString(R.string.preference_screen_zoom_fit_name),
|
||||
title = stringResource(R.string.preference_screen_zoom_fit_name),
|
||||
preferenceKey = PreferenceUtils.Key.SCREEN_ZOOM_FIT,
|
||||
asCard = false
|
||||
)
|
||||
@@ -393,30 +375,51 @@ fun SettingsScreen(
|
||||
maxFrameRate = maxFrameRate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OptionsGroup(context.getString(R.string.preference_category_updater)) {
|
||||
@Composable
|
||||
fun UpdateSettingsGroup(releaseViewModel: ReleaseViewModel, snackbarHostState: SnackbarHostState, onUpdate: () -> Unit) {
|
||||
val updateStatus by releaseViewModel.uiState.collectAsState()
|
||||
|
||||
OptionsGroup(stringResource(R.string.preference_category_updater)) {
|
||||
SettingsCard(
|
||||
title = context.getString(R.string.preference_update_automatically_name),
|
||||
title = stringResource(R.string.preference_update_automatically_name),
|
||||
preferenceKey = PreferenceUtils.Key.UPDATE_AUTOMATICALLY,
|
||||
)
|
||||
|
||||
val lastUpdateCheck by PreferenceUtils.useLongPreference(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME)
|
||||
val checkAgoString = remember(lastUpdateCheck) {
|
||||
if (lastUpdateCheck != 0L) {
|
||||
val now = Clock.System.now().toEpochMilliseconds()
|
||||
DateUtils.getRelativeTimeSpanString(
|
||||
lastUpdateCheck,
|
||||
now,
|
||||
DateUtils.MINUTE_IN_MILLIS,
|
||||
DateUtils.FORMAT_ABBREV_RELATIVE
|
||||
)
|
||||
} else {
|
||||
"never"
|
||||
}
|
||||
}
|
||||
|
||||
OptionsCard(
|
||||
title = {
|
||||
OptionsTitle(
|
||||
title = stringResource(R.string.preference_check_for_updates_button),
|
||||
description = stringResource(
|
||||
R.string.preference_check_for_updates_description,
|
||||
currentRelease ?: "unknown"
|
||||
icon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_update),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
description = stringResource(R.string.preference_check_for_updates_description, checkAgoString)
|
||||
.takeIf { lastUpdateCheck != 0L }
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
onClick = {
|
||||
if (releaseViewModel.isInUpdate) {
|
||||
showUpdateInProgress = true
|
||||
} else {
|
||||
releaseViewModel.runReleaseCheck(true)
|
||||
}
|
||||
onUpdate()
|
||||
},
|
||||
role = Role.Button
|
||||
)
|
||||
@@ -424,53 +427,114 @@ fun SettingsScreen(
|
||||
UpdateIndicator(snackbarHostState, updateStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AdvancedSettingsGroup(developerModeEnabled: Boolean) {
|
||||
val context = LocalContext.current
|
||||
|
||||
OptionsGroup(stringResource(R.string.preference_category_developer)) {
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preferences_view_logs),
|
||||
icon = {
|
||||
Icon(painterResource(R.drawable.icon_description), contentDescription = null)
|
||||
},
|
||||
onClick = { onOpenLogs(context) }
|
||||
)
|
||||
if (developerModeEnabled) {
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preference_open_developer_options),
|
||||
icon = {
|
||||
Icon(painterResource(R.drawable.icon_data_object), contentDescription = null)
|
||||
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) }
|
||||
)
|
||||
}
|
||||
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preferences_view_crashes),
|
||||
icon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_bug_report),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
val launchIntent = Intent(context, CrashLogsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
)
|
||||
|
||||
var showSafeModeDialog by remember { mutableStateOf(false) }
|
||||
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preference_launch_safe_mode),
|
||||
icon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_shield),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
) {
|
||||
showSafeModeDialog = true
|
||||
}
|
||||
|
||||
if (showSafeModeDialog) {
|
||||
SafeModeDialog(onDismiss = {
|
||||
showSafeModeDialog = false
|
||||
}) {
|
||||
val launchIntent = Intent(context, MainActivity::class.java).apply {
|
||||
// hardcode it, why not
|
||||
action = Intent.ACTION_VIEW
|
||||
data = "geode-launcher://main/launch?safe-mode=true".toUri()
|
||||
}
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AboutSettingsGroup(developerModeEnabled: Boolean) {
|
||||
val context = LocalContext.current
|
||||
val resources = LocalResources.current
|
||||
val currentRelease by PreferenceUtils.useStringPreference(PreferenceUtils.Key.CURRENT_VERSION_TAG)
|
||||
|
||||
OptionsGroup(stringResource(R.string.preference_category_about)) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
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
|
||||
) {
|
||||
clipboard.setText(AnnotatedString(context.getString(
|
||||
R.string.preference_launcher_version,
|
||||
BuildConfig.VERSION_NAME
|
||||
)))
|
||||
launcherButtonTapped += 1
|
||||
|
||||
if (launcherButtonTapped >= 7) {
|
||||
if (developerModeEnabled) {
|
||||
Toast.makeText(context, R.string.preference_enable_developer_again, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
} else {
|
||||
showDeveloperDialog = true
|
||||
}
|
||||
launcherButtonTapped = 0
|
||||
}
|
||||
}
|
||||
|
||||
OptionsButton(
|
||||
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
|
||||
) {
|
||||
clipboard.setText(AnnotatedString(context.getString(
|
||||
R.string.preference_loader_version,
|
||||
currentRelease ?: "unknown"
|
||||
)))
|
||||
}
|
||||
)
|
||||
|
||||
val gameInstalled = remember {
|
||||
GamePackageUtils.isGameInstalled(context.packageManager)
|
||||
@@ -482,38 +546,108 @@ fun SettingsScreen(
|
||||
}
|
||||
|
||||
val gameSource = remember {
|
||||
context.getString(when (GamePackageUtils.identifyGameSource(context.packageManager)) {
|
||||
resources.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
|
||||
})
|
||||
}
|
||||
|
||||
OptionsButton(
|
||||
OptionsLabel(
|
||||
stringResource(R.string.preference_game_version_name),
|
||||
stringResource(R.string.preference_game_version_description, gameVersion, gameSource),
|
||||
displayInline = true
|
||||
) {
|
||||
clipboard.setText(AnnotatedString(context.getString(
|
||||
R.string.preference_game_version,
|
||||
gameVersion,
|
||||
gameSource
|
||||
)))
|
||||
}
|
||||
)
|
||||
|
||||
OptionsButton(
|
||||
OptionsLabel(
|
||||
stringResource(R.string.preference_architecture_name),
|
||||
stringResource(R.string.preference_architecture_description, Build.MODEL, LaunchUtils.applicationArchitecture),
|
||||
displayInline = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
onBackPressedDispatcher: OnBackPressedDispatcher?,
|
||||
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
|
||||
) {
|
||||
clipboard.setText(AnnotatedString(context.getString(
|
||||
R.string.preference_architecture,
|
||||
Build.MODEL,
|
||||
LaunchUtils.applicationArchitecture
|
||||
)))
|
||||
val resources = LocalResources.current
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
var showUpdateInProgress by remember { mutableStateOf(false) }
|
||||
|
||||
if (showUpdateInProgress) {
|
||||
LaunchedEffect(snackbarHostState) {
|
||||
snackbarHostState.showSnackbar(
|
||||
resources.getString(R.string.preference_check_for_updates_already_updating)
|
||||
)
|
||||
showUpdateInProgress = false
|
||||
}
|
||||
}
|
||||
|
||||
// fix theme transition by giving it the exact same animation as the top bar
|
||||
val containerColor by animateColorAsState(
|
||||
targetValue = MaterialTheme.colorScheme.background,
|
||||
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
|
||||
label = "background color"
|
||||
)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
containerColor = containerColor,
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = snackbarHostState)
|
||||
},
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_arrow_back),
|
||||
contentDescription = stringResource(R.string.back_icon_alt)
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.title_activity_settings),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.background),
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
GeneralSettingsGroup()
|
||||
GameplaySettingsGroup()
|
||||
|
||||
UpdateSettingsGroup(releaseViewModel, snackbarHostState, onUpdate = {
|
||||
if (releaseViewModel.isInUpdate) {
|
||||
showUpdateInProgress = true
|
||||
} else {
|
||||
releaseViewModel.runReleaseCheck(true)
|
||||
}
|
||||
})
|
||||
|
||||
val developerModeEnabled by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
|
||||
|
||||
AdvancedSettingsGroup(developerModeEnabled)
|
||||
AboutSettingsGroup(developerModeEnabled)
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3,10 +3,12 @@ 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.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
@@ -18,12 +20,9 @@ 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.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -46,6 +45,8 @@ 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.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -82,16 +83,24 @@ 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 {
|
||||
if (title != null) {
|
||||
Text(
|
||||
title,
|
||||
style = Typography.titleMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsSelectCard(
|
||||
@@ -238,12 +247,16 @@ fun StringDialog(
|
||||
) {
|
||||
var enteredValue by remember { mutableStateOf(initialValue) }
|
||||
|
||||
AlertDialog(
|
||||
BaseDialog(
|
||||
onDismissRequest = { onDismissRequest() },
|
||||
title = {
|
||||
Text(title)
|
||||
},
|
||||
text = {
|
||||
title = title,
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = enteredValue,
|
||||
onValueChange = {
|
||||
@@ -258,13 +271,15 @@ fun StringDialog(
|
||||
if (enteredValue.isNotEmpty()) {
|
||||
IconButton(onClick = { enteredValue = "" }) {
|
||||
Icon(
|
||||
Icons.Filled.Clear,
|
||||
painterResource(R.drawable.icon_close_small),
|
||||
contentDescription = stringResource(R.string.preference_text_clear)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onSelect(enteredValue) }) {
|
||||
@@ -295,17 +310,14 @@ fun RangeDialog(
|
||||
mutableFloatStateOf(initialValue / scale.toFloat())
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
BaseDialog(
|
||||
onDismissRequest = { onDismissRequest() },
|
||||
title = {
|
||||
Text(title)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
title = title,
|
||||
content = {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1.0f, false)
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
) {
|
||||
val precision = log10(scale.toFloat()).roundToInt()
|
||||
|
||||
@@ -321,8 +333,6 @@ fun RangeDialog(
|
||||
}
|
||||
|
||||
children()
|
||||
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onSelect((enteredValue * scale).roundToInt()) }) {
|
||||
@@ -357,16 +367,16 @@ fun FrameRateDialog(
|
||||
val maximumReached = currentValue != null && currentValue > maxFrameRate
|
||||
val minimumReached = currentValue != null && currentValue < minFrameRate
|
||||
|
||||
AlertDialog(
|
||||
BaseDialog(
|
||||
onDismissRequest = { onDismissRequest() },
|
||||
title = {
|
||||
Text(title)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
title = title,
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1.0f, false)
|
||||
) {
|
||||
IconButton(onClick = {
|
||||
val prevValue = max((currentValue ?: 0) - 5, minFrameRate)
|
||||
@@ -406,7 +416,7 @@ fun FrameRateDialog(
|
||||
enteredValue = nextValue.toString()
|
||||
}, enabled = currentValue == null || currentValue < maxFrameRate) {
|
||||
Icon(
|
||||
Icons.Filled.Add,
|
||||
painterResource(R.drawable.icon_add),
|
||||
contentDescription = stringResource(R.string.preference_limit_framerate_add)
|
||||
)
|
||||
}
|
||||
@@ -619,16 +629,14 @@ fun ProfileCreateDialog(onDismissRequest: () -> Unit) {
|
||||
|
||||
val isDuplicate = currentProfiles.contains(filename)
|
||||
|
||||
AlertDialog(
|
||||
BaseDialog(
|
||||
onDismissRequest = { onDismissRequest() },
|
||||
title = {
|
||||
Text(stringResource(R.string.preference_profiles_create))
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1.0f, false)
|
||||
title = stringResource(R.string.preference_profiles_create),
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = enteredValue,
|
||||
@@ -642,9 +650,9 @@ fun ProfileCreateDialog(onDismissRequest: () -> Unit) {
|
||||
label = {
|
||||
Text(stringResource(R.string.preference_profiles_create_name))
|
||||
},
|
||||
isError = isDuplicate
|
||||
isError = isDuplicate,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
}
|
||||
|
||||
if (isDuplicate) {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
@@ -749,8 +757,8 @@ fun ProfileSelectCard() {
|
||||
.setCurrentProfile(selectedProfileId)
|
||||
|
||||
activity?.run {
|
||||
packageManager.getLaunchIntentForPackage(packageName)?.also {
|
||||
val mainIntent = Intent.makeRestartActivityTask(it.component)
|
||||
packageManager.getLaunchIntentForPackage(packageName)?.also { intent ->
|
||||
val mainIntent = Intent.makeRestartActivityTask(intent.component)
|
||||
startActivity(mainIntent)
|
||||
exitProcess(0)
|
||||
}
|
||||
@@ -783,7 +791,7 @@ fun ProfileSelectCard() {
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
painterResource(R.drawable.icon_delete),
|
||||
contentDescription = stringResource(R.string.preference_profiles_delete_alt)
|
||||
)
|
||||
}
|
||||
@@ -795,6 +803,54 @@ fun ProfileSelectCard() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BaseDialog(
|
||||
title: String,
|
||||
onDismissRequest: () -> Unit,
|
||||
dismissButton: @Composable () -> Unit,
|
||||
confirmButton: @Composable () -> Unit,
|
||||
content: @Composable (ColumnScope.() -> Unit),
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
)
|
||||
) {
|
||||
// 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(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text(
|
||||
title,
|
||||
style = Typography.titleLarge,
|
||||
modifier = Modifier.padding(
|
||||
start = 28.dp,
|
||||
top = 24.dp,
|
||||
bottom = 12.dp
|
||||
)
|
||||
)
|
||||
|
||||
content()
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
bottom = 16.dp,
|
||||
end = 16.dp,
|
||||
top = 4.dp
|
||||
)
|
||||
) {
|
||||
dismissButton()
|
||||
confirmButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> SelectDialog(
|
||||
title: String,
|
||||
@@ -805,23 +861,20 @@ fun <T> SelectDialog(
|
||||
) {
|
||||
val (selectedValue, setSelectedValue) = remember { mutableStateOf(initialValue) }
|
||||
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
BaseDialog(
|
||||
title = title,
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.message_box_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onSelect(selectedValue) }) {
|
||||
Text(stringResource(R.string.message_box_accept))
|
||||
}
|
||||
}
|
||||
) {
|
||||
// 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(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text(
|
||||
title,
|
||||
style = Typography.titleLarge,
|
||||
modifier = Modifier.padding(
|
||||
start = 28.dp,
|
||||
top = 24.dp,
|
||||
bottom = 12.dp
|
||||
)
|
||||
)
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalSelectValue provides (selectedValue as Any),
|
||||
LocalSelectSetValue provides ({
|
||||
@@ -831,27 +884,6 @@ fun <T> SelectDialog(
|
||||
) {
|
||||
options()
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
bottom = 16.dp,
|
||||
end = 16.dp,
|
||||
top = 4.dp
|
||||
)
|
||||
) {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.message_box_cancel))
|
||||
}
|
||||
|
||||
TextButton(onClick = { onSelect(selectedValue) }) {
|
||||
Text(stringResource(R.string.message_box_accept))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -875,6 +907,24 @@ fun OptionsButton(title: String, description: String? = null, icon: (@Composable
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
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
|
||||
@@ -890,24 +940,25 @@ fun SettingsCard(title: String, description: String? = null, icon: (@Composable
|
||||
icon = icon
|
||||
)
|
||||
},
|
||||
modifier = if (asCard) Modifier.toggleable(
|
||||
modifier = Modifier.toggleable(
|
||||
value = settingEnabled.value,
|
||||
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceKey) },
|
||||
role = Role.Switch,
|
||||
) else Modifier
|
||||
),
|
||||
backgroundColor = if (asCard)
|
||||
MaterialTheme.colorScheme.surfaceContainer
|
||||
else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
) {
|
||||
Switch(
|
||||
checked = settingEnabled.value,
|
||||
onCheckedChange = if (!asCard)
|
||||
{ _ -> settingEnabled.value = toggleSetting(context, preferenceKey) }
|
||||
else null
|
||||
onCheckedChange = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: String? = null, icon: (@Composable () -> Unit)? = null) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (icon != null) {
|
||||
icon()
|
||||
}
|
||||
@@ -920,7 +971,7 @@ fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: Stri
|
||||
Text(
|
||||
description,
|
||||
style = Typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -928,10 +979,20 @@ fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: Stri
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OptionsCard(modifier: Modifier = Modifier, wrapContent: Boolean = false, title: @Composable () -> Unit, content: @Composable () -> Unit) {
|
||||
fun OptionsCard(
|
||||
modifier: Modifier = Modifier,
|
||||
wrapContent: Boolean = false,
|
||||
backgroundColor: Color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
title: @Composable () -> Unit,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 1.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.then(modifier)
|
||||
.background(backgroundColor)
|
||||
.defaultMinSize(minHeight = 64.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
Arrangement.SpaceBetween,
|
||||
@@ -950,6 +1011,31 @@ fun OptionsCard(modifier: Modifier = Modifier, wrapContent: Boolean = false, tit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SafeModeDialog(onDismiss: () -> Unit, onLaunch: () -> Unit) {
|
||||
AlertDialog(
|
||||
icon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_shield_alert),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
title = { Text(stringResource(R.string.safe_mode_enable_title)) },
|
||||
text = { Text(stringResource(R.string.safe_mode_enable_description)) },
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.message_box_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onLaunch) {
|
||||
Text(stringResource(R.string.message_box_continue))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InlineText(label: String, icon: @Composable (() -> Unit)? = null, modifier: Modifier = Modifier) {
|
||||
LabelledText(label = label, icon = icon, modifier = modifier.padding(horizontal = 16.dp, vertical = 4.dp))
|
||||
|
||||
@@ -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,6 +1,5 @@
|
||||
package com.geode.launcher.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -9,14 +8,9 @@ import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
@@ -28,16 +22,6 @@ private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
enum class Theme {
|
||||
|
||||
@@ -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
|
||||
data class Release(
|
||||
data class Release @OptIn(ExperimentalTime::class) constructor(
|
||||
val url: String,
|
||||
val id: Int,
|
||||
val targetCommitish: String,
|
||||
@@ -30,7 +31,7 @@ data class Release(
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LoaderVersion(
|
||||
data class LoaderVersion @OptIn(ExperimentalTime::class) constructor(
|
||||
val tag: String,
|
||||
val version: String,
|
||||
val createdAt: Instant,
|
||||
@@ -66,6 +67,7 @@ class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadab
|
||||
return release.tagName
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override fun getDescriptor(): Long {
|
||||
return release.createdAt.epochSeconds
|
||||
}
|
||||
@@ -95,6 +97,7 @@ class DownloadableLauncherRelease(val release: Release) : Downloadable() {
|
||||
return release.tagName
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override fun getDescriptor(): Long {
|
||||
return release.createdAt.epochSeconds
|
||||
}
|
||||
@@ -132,6 +135,7 @@ class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadab
|
||||
return version.tag
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override fun getDescriptor(): Long {
|
||||
return version.createdAt.epochSeconds
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import okio.buffer
|
||||
import okio.source
|
||||
import java.io.File
|
||||
import java.io.InterruptedIOException
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
private const val TAG_LATEST = "latest"
|
||||
private const val TAG_BETA = "prerelease"
|
||||
@@ -120,19 +122,21 @@ class ReleaseManager private constructor(
|
||||
}
|
||||
|
||||
val gameVersion = GamePackageUtils.getGameVersionCode(applicationContext.packageManager)
|
||||
val targetTag = getBestReleaseForGameVersion(gameVersion) ?: return null
|
||||
val preferences = PreferenceUtils.get(applicationContext)
|
||||
|
||||
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: Downloadable) {
|
||||
val download = release.getDownload() ?: return
|
||||
|
||||
val outputDirectory = LaunchUtils.getBaseDirectory(applicationContext)
|
||||
val outputDirectory = applicationContext.filesDir
|
||||
val outputFile = File(outputDirectory, download.filename)
|
||||
|
||||
if (outputFile.exists()) {
|
||||
@@ -169,6 +173,10 @@ class ReleaseManager private constructor(
|
||||
// set an initial download size
|
||||
_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,
|
||||
@@ -177,23 +185,30 @@ class ReleaseManager private constructor(
|
||||
_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()
|
||||
@@ -232,6 +247,16 @@ class ReleaseManager private constructor(
|
||||
}
|
||||
|
||||
private suspend fun checkForNewRelease(isManual: Boolean = false) {
|
||||
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
||||
val lastCheckTime = sharedPreferences.getLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME)
|
||||
|
||||
// only check for updates if it's been over 15 minutes since last check
|
||||
val checkMinTime = Clock.System.now().minus(15.minutes).toEpochMilliseconds()
|
||||
if (!isManual && lastCheckTime > checkMinTime && !fileWasExternallyModified()) {
|
||||
_uiState.value = ReleaseManagerState.Finished()
|
||||
return
|
||||
}
|
||||
|
||||
val release = try {
|
||||
getLatestRelease()
|
||||
} catch (e: Exception) {
|
||||
@@ -250,6 +275,9 @@ class ReleaseManager private constructor(
|
||||
checkLauncherUpdate(launcherRelease)
|
||||
}
|
||||
|
||||
val currentTime = Clock.System.now().toEpochMilliseconds()
|
||||
sharedPreferences.setLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME, currentTime)
|
||||
|
||||
if (release == null) {
|
||||
downloadLauncherUpdateIfNecessary()
|
||||
|
||||
@@ -257,8 +285,6 @@ class ReleaseManager private constructor(
|
||||
return
|
||||
}
|
||||
|
||||
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
||||
|
||||
val currentVersion = sharedPreferences.getLong(PreferenceUtils.Key.CURRENT_VERSION_TIMESTAMP)
|
||||
val latestVersion = release.getDescriptor()
|
||||
|
||||
@@ -313,6 +339,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.
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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
|
||||
@@ -14,6 +14,7 @@ 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 {
|
||||
@@ -28,7 +29,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
}
|
||||
|
||||
suspend fun getLatestLauncherRelease(): DownloadableLauncherRelease? {
|
||||
val releasePath = "$GITHUB_API_BASE/repos/Lncvrt/xps-geode-launcher/releases/latest"
|
||||
val releasePath = "$GITHUB_API_BASE/repos/geode-sdk/android-launcher/releases/latest"
|
||||
|
||||
val url = URL(releasePath)
|
||||
|
||||
@@ -49,7 +50,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
return getReleaseByUrl(url)?.let(::DownloadableGitHubLoaderRelease)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@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)
|
||||
@@ -104,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)
|
||||
@@ -113,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)
|
||||
@@ -165,10 +167,10 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
suspend fun getLatestIndexRelease(gd: String, prerelease: Boolean = false): DownloadableLoaderRelease? {
|
||||
suspend fun getLatestIndexRelease(gd: Long, prerelease: Boolean = false): DownloadableLoaderRelease? {
|
||||
val url = "$GEODE_API_BASE/loader/versions/latest".toHttpUrlOrNull()!!
|
||||
.newBuilder()
|
||||
.addQueryParameter("gd", gd)
|
||||
.addQueryParameter("gd", gd.toString())
|
||||
.addQueryParameter("platform", "android")
|
||||
.addQueryParameter("prerelease", prerelease.toString())
|
||||
.build()
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.geode.launcher.utils
|
||||
import android.annotation.SuppressLint
|
||||
|
||||
object Constants {
|
||||
const val PACKAGE_NAME = "xyz.lncvrt.xps0000002200"
|
||||
const val PACKAGE_NAME = "com.robtopx.geometryjump"
|
||||
|
||||
const val COCOS_LIB_NAME = "cocos2dcpp"
|
||||
const val FMOD_LIB_NAME = "fmod"
|
||||
|
||||
@@ -51,7 +51,8 @@ object GamePackageUtils {
|
||||
37L to "2.200",
|
||||
38L to "2.205",
|
||||
39L to "2.206",
|
||||
40L to "2.2074" // ok
|
||||
40L to "2.2074", // ok
|
||||
41L to "2.2081",
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,9 @@ 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
|
||||
@@ -16,12 +19,15 @@ 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
|
||||
@@ -37,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>
|
||||
@@ -386,6 +394,9 @@ object GeodeUtils {
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current version code of the launcher.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getLauncherVersion(): Int {
|
||||
return BuildConfig.VERSION_CODE
|
||||
@@ -457,11 +468,22 @@ object GeodeUtils {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -470,6 +492,10 @@ object GeodeUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
@@ -480,6 +506,19 @@ object GeodeUtils {
|
||||
}
|
||||
}
|
||||
|
||||
@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>()
|
||||
@@ -511,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)
|
||||
|
||||
@@ -518,6 +558,10 @@ 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()) {
|
||||
@@ -527,11 +571,220 @@ object GeodeUtils {
|
||||
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)
|
||||
external fun nativeKeyDown(keyCode: Int, modifiers: Int, isRepeating: Boolean)
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -73,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/")
|
||||
}
|
||||
|
||||
@@ -139,7 +139,6 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
LIMIT_ASPECT_RATIO,
|
||||
DISPLAY_MODE,
|
||||
FORCE_HRR,
|
||||
ENABLE_REDESIGN,
|
||||
RELEASE_CHANNEL_TAG,
|
||||
DEVELOPER_MODE,
|
||||
CLEANUP_APKS,
|
||||
@@ -147,7 +146,12 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
CUSTOM_ASPECT_RATIO,
|
||||
SCREEN_ZOOM,
|
||||
SCREEN_ZOOM_FIT,
|
||||
LIMIT_FRAME_RATE
|
||||
LIMIT_FRAME_RATE,
|
||||
DISABLE_USER_THEME,
|
||||
SELECTED_ICON,
|
||||
DISABLE_REDESIGN,
|
||||
WAIT_PERIOD,
|
||||
LAST_UPDATE_CHECK_TIME
|
||||
}
|
||||
|
||||
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
||||
@@ -161,12 +165,14 @@ 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
|
||||
Key.WAIT_PERIOD -> 3
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -186,7 +192,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
Key.LIMIT_ASPECT_RATIO -> "PreferenceLimitAspectRatio"
|
||||
Key.DISPLAY_MODE -> "PreferenceDisplayMode"
|
||||
Key.FORCE_HRR -> "PreferenceForceHighRefreshRate"
|
||||
Key.ENABLE_REDESIGN -> "PreferenceEnableRedesign"
|
||||
Key.DISABLE_REDESIGN -> "PreferenceDisableRedesign"
|
||||
Key.RELEASE_CHANNEL_TAG -> "PreferenceReleaseChannelTag"
|
||||
Key.DEVELOPER_MODE -> "PreferenceDeveloperMode"
|
||||
Key.CLEANUP_APKS -> "PreferenceCleanupPackages"
|
||||
@@ -195,6 +201,10 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
Key.SCREEN_ZOOM -> "PreferenceScreenZoom"
|
||||
Key.SCREEN_ZOOM_FIT -> "PreferenceScreenZoomFit"
|
||||
Key.LIMIT_FRAME_RATE -> "PreferenceLimitFrameRate"
|
||||
Key.DISABLE_USER_THEME -> "PreferenceDisableUserTheme"
|
||||
Key.SELECTED_ICON -> "PreferenceSelectedIcon"
|
||||
Key.WAIT_PERIOD -> "PreferenceWaitPeriod"
|
||||
Key.LAST_UPDATE_CHECK_TIME -> "PreferenceLastUpdateCheckTime"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -2,23 +2,28 @@ 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
|
||||
private const val MS_TO_NS = 1_000_000
|
||||
private const val SEC_TO_NS = 1_000_000_000.0
|
||||
|
||||
class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
companion object {
|
||||
@@ -55,13 +60,15 @@ 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()
|
||||
}
|
||||
|
||||
@@ -149,6 +156,11 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
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)
|
||||
@@ -156,6 +168,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
val ys = FloatArray(pointerNumber)
|
||||
val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
motionEvent.eventTimeNanos else motionEvent.eventTime * MS_TO_NS
|
||||
val timestampSec = timestamp / SEC_TO_NS
|
||||
|
||||
for (i in 0 until pointerNumber) {
|
||||
ids[i] = motionEvent.getPointerId(i)
|
||||
@@ -168,10 +181,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, timestampSec)
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -180,28 +199,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, timestampSec)
|
||||
}
|
||||
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, timestampSec)
|
||||
}
|
||||
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, timestampSec)
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -211,10 +245,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, timestampSec)
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -224,10 +263,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, timestampSec)
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -259,8 +303,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)
|
||||
}
|
||||
|
||||
@@ -275,18 +362,23 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
}
|
||||
|
||||
queueEvent {
|
||||
if (sendTimestampEvents)
|
||||
GeodeUtils.setNextInputTimestamp(event.eventTime * MS_TO_NS)
|
||||
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
|
||||
if (!useKeyboardEvents) {
|
||||
if (!useKeyboardEvents && !controllerEventsEnabled) {
|
||||
return super.onKeyUp(keyCode, event)
|
||||
}
|
||||
|
||||
@@ -300,19 +392,83 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
}
|
||||
|
||||
queueEvent {
|
||||
if (sendTimestampEvents)
|
||||
GeodeUtils.setNextInputTimestamp(event.eventTime * MS_TO_NS)
|
||||
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -323,12 +479,42 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
event.eventTimeNanos else event.eventTime * MS_TO_NS
|
||||
|
||||
queueEvent {
|
||||
if (sendTimestampEvents)
|
||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
||||
sendNextEventTimestamp(timestamp)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -351,4 +537,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,21 +40,25 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi
|
||||
@JvmStatic
|
||||
private external fun nativeTextClosed()
|
||||
|
||||
// timestamp was added in 2.208, but the jni method has no required type signature.
|
||||
// it doesn't hurt to unconditionally send one
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeTouchesBegin(id: Int, x: Float, y: Float)
|
||||
private external fun nativeTouchesBegin(id: Int, x: Float, y: Float, timestamp: Double)
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeTouchesCancel(
|
||||
ids: IntArray,
|
||||
xs: FloatArray,
|
||||
ys: FloatArray
|
||||
ys: FloatArray,
|
||||
timestamp: Double,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeTouchesEnd(id: Int, x: Float, y: Float)
|
||||
private external fun nativeTouchesEnd(id: Int, x: Float, y: Float, timestamp: Double)
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeTouchesMove(ids: IntArray, xs: FloatArray, ys: FloatArray)
|
||||
private external fun nativeTouchesMove(ids: IntArray, xs: FloatArray, ys: FloatArray, timestamp: Double)
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeInit(width: Int, height: Int)
|
||||
@@ -109,20 +113,20 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi
|
||||
}
|
||||
}
|
||||
|
||||
fun handleActionDown(id: Int, x: Float, y: Float) {
|
||||
nativeTouchesBegin(id, x, y)
|
||||
fun handleActionDown(id: Int, x: Float, y: Float, timestamp: Double) {
|
||||
nativeTouchesBegin(id, x, y, timestamp)
|
||||
}
|
||||
|
||||
fun handleActionUp(id: Int, x: Float, y: Float) {
|
||||
nativeTouchesEnd(id, x, y)
|
||||
fun handleActionUp(id: Int, x: Float, y: Float, timestamp: Double) {
|
||||
nativeTouchesEnd(id, x, y, timestamp)
|
||||
}
|
||||
|
||||
fun handleActionCancel(ids: IntArray, xs: FloatArray, ys: FloatArray) {
|
||||
nativeTouchesCancel(ids, xs, ys)
|
||||
fun handleActionCancel(ids: IntArray, xs: FloatArray, ys: FloatArray, timestamp: Double) {
|
||||
nativeTouchesCancel(ids, xs, ys, timestamp)
|
||||
}
|
||||
|
||||
fun handleActionMove(ids: IntArray, xs: FloatArray, ys: FloatArray) {
|
||||
nativeTouchesMove(ids, xs, ys)
|
||||
fun handleActionMove(ids: IntArray, xs: FloatArray, ys: FloatArray, timestamp: Double) {
|
||||
nativeTouchesMove(ids, xs, ys, timestamp)
|
||||
}
|
||||
|
||||
fun handleKeyDown(keyCode: Int) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.AssetManager
|
||||
import android.media.AudioDeviceCallback
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
@@ -14,27 +15,45 @@ import android.os.Build
|
||||
import android.util.Log
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
|
||||
// i never agreed to your licenses! take that
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object FMOD {
|
||||
private var gContext: Context? = null
|
||||
private var gPluginAudioDeviceCallback: PluginAudioDeviceCallback? = null
|
||||
private val gPluginBroadcastReceiver = PluginBroadcastReceiver()
|
||||
|
||||
external fun OutputAAudioHeadphonesChanged()
|
||||
external fun SetInputEnumerationChanged()
|
||||
external fun SetOutputEnumerationChanged()
|
||||
|
||||
@JvmStatic
|
||||
fun init(context: Context?) {
|
||||
gContext = context
|
||||
gContext?.registerReceiver(
|
||||
gPluginBroadcastReceiver,
|
||||
IntentFilter(Intent.ACTION_HEADSET_PLUG)
|
||||
)
|
||||
if (context == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val intentFilter = IntentFilter(Intent.ACTION_HEADSET_PLUG)
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
context.registerReceiver(gPluginBroadcastReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
|
||||
} else {
|
||||
context.registerReceiver(gPluginBroadcastReceiver, intentFilter)
|
||||
}
|
||||
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
val pluginAudioDeviceCallback = PluginAudioDeviceCallback(audioManager.getDevices(3))
|
||||
gPluginAudioDeviceCallback = pluginAudioDeviceCallback
|
||||
audioManager.registerAudioDeviceCallback(pluginAudioDeviceCallback, null)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun close() {
|
||||
val context = gContext
|
||||
context?.unregisterReceiver(gPluginBroadcastReceiver)
|
||||
if (context != null) {
|
||||
context.unregisterReceiver(gPluginBroadcastReceiver)
|
||||
(context.getSystemService(Context.AUDIO_SERVICE) as AudioManager)
|
||||
.unregisterAudioDeviceCallback(gPluginAudioDeviceCallback)
|
||||
}
|
||||
gContext = null
|
||||
}
|
||||
|
||||
@@ -109,6 +128,12 @@ object FMOD {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getAudioDevices(i: Int): Array<AudioDeviceInfo?>? {
|
||||
val context = gContext ?: return emptyArray()
|
||||
return (context.getSystemService(Context.AUDIO_SERVICE) as AudioManager).getDevices(i)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun fileDescriptorFromUri(str: String?): Int {
|
||||
gContext?.apply {
|
||||
@@ -127,4 +152,82 @@ object FMOD {
|
||||
OutputAAudioHeadphonesChanged()
|
||||
}
|
||||
}
|
||||
|
||||
internal class PluginAudioDeviceCallback : AudioDeviceCallback {
|
||||
val deviceSet: HashSet<Int> = HashSet()
|
||||
|
||||
constructor(audioDeviceInfoArr: Array<out AudioDeviceInfo?>?) {
|
||||
if (audioDeviceInfoArr != null) {
|
||||
deviceSet.addAll(audioDeviceInfoArr.mapNotNull { it?.id })
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAudioDevicesAdded(audioDeviceInfoArr: Array<out AudioDeviceInfo?>?) {
|
||||
var z: Boolean
|
||||
var i = 0
|
||||
if (audioDeviceInfoArr != null) {
|
||||
var i2 = 0
|
||||
z = false
|
||||
while (i < audioDeviceInfoArr.size) {
|
||||
if (!deviceSet.contains(audioDeviceInfoArr[i]!!.id)) {
|
||||
if (audioDeviceInfoArr[i]!!.isSource) {
|
||||
i2 = 1
|
||||
}
|
||||
if (audioDeviceInfoArr[i]!!.isSink) {
|
||||
z = true
|
||||
}
|
||||
deviceSet.add(audioDeviceInfoArr[i]!!.id)
|
||||
}
|
||||
i++
|
||||
}
|
||||
i = i2
|
||||
} else {
|
||||
z = false
|
||||
}
|
||||
if (i != 0) {
|
||||
try {
|
||||
SetInputEnumerationChanged()
|
||||
} catch (_: UnsatisfiedLinkError) { }
|
||||
}
|
||||
if (z) {
|
||||
try {
|
||||
SetOutputEnumerationChanged()
|
||||
} catch (_: UnsatisfiedLinkError) { }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAudioDevicesRemoved(audioDeviceInfoArr: Array<out AudioDeviceInfo?>?) {
|
||||
var z: Boolean
|
||||
var i = 0
|
||||
if (audioDeviceInfoArr != null) {
|
||||
var i2 = 0
|
||||
z = false
|
||||
while (i < audioDeviceInfoArr.size) {
|
||||
if (deviceSet.contains(audioDeviceInfoArr[i]!!.id)) {
|
||||
if (audioDeviceInfoArr[i]!!.isSource) {
|
||||
i2 = 1
|
||||
}
|
||||
if (audioDeviceInfoArr[i]!!.isSink) {
|
||||
z = true
|
||||
}
|
||||
deviceSet.remove(audioDeviceInfoArr[i]!!.id)
|
||||
}
|
||||
i++
|
||||
}
|
||||
i = i2
|
||||
} else {
|
||||
z = false
|
||||
}
|
||||
if (i != 0) {
|
||||
try {
|
||||
SetInputEnumerationChanged()
|
||||
} catch (_: UnsatisfiedLinkError) { }
|
||||
}
|
||||
if (z) {
|
||||
try {
|
||||
SetOutputEnumerationChanged()
|
||||
} catch (_: UnsatisfiedLinkError) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,43 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="204dp"
|
||||
android:height="205dp"
|
||||
android:viewportWidth="204"
|
||||
android:viewportHeight="205">
|
||||
<path
|
||||
android:pathData="M114.92,6.98L109,2.47C104.7,-0.82 98.72,-0.82 94.42,2.47L4.72,70.96C0.72,74.02 -0.95,79.25 0.54,84.06L35.03,195.65C36.59,200.69 41.24,204.12 46.51,204.12H156.91C162.18,204.12 166.83,200.69 168.38,195.65L202.88,84.06C204.37,79.25 202.7,74.02 198.7,70.96L190.43,64.65L180.92,94.56L154.79,179.53C153.77,182.85 150.71,185.12 147.23,185.12H55.6C52.13,185.12 49.06,182.85 48.04,179.53L19.44,86.54C18.47,83.38 19.57,79.95 22.19,77.94L96.6,20.81C96.9,20.58 97.21,20.38 97.53,20.19L114.92,6.98Z"
|
||||
android:fillType="evenOdd">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="65.53"
|
||||
android:startY="18.28"
|
||||
android:endX="150.22"
|
||||
android:endY="187.89"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FFFFFDFF"/>
|
||||
<item android:offset="0.08" android:color="#FFF2EAF5"/>
|
||||
<item android:offset="0.12" android:color="#FFEDE5EF"/>
|
||||
<item android:offset="0.31" android:color="#FFCDB5CD"/>
|
||||
<item android:offset="0.49" android:color="#FFBA9BBC"/>
|
||||
<item android:offset="1" android:color="#FF8D7ACF"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M98.37,41.8C100.69,40.03 103.9,40.03 106.22,41.8L161.71,84.35C163.85,85.98 164.74,88.78 163.95,91.36L142.61,160.64C141.78,163.35 139.28,165.19 136.45,165.19H68.14C65.31,165.19 62.81,163.35 61.98,160.64L58.33,148.8H76.42C76.63,148.84 76.85,148.87 77.07,148.87H126.35C127.77,148.87 129.03,147.93 129.44,146.57L144.82,95.62C145.2,94.35 144.76,92.97 143.72,92.15L103.69,60.85C102.53,59.94 100.89,59.94 99.72,60.85L63.01,89.56L44.33,103.33L40.65,91.36C39.85,88.78 40.75,85.98 42.88,84.35L98.37,41.8Z"
|
||||
android:fillType="evenOdd">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="102.3"
|
||||
android:startY="-83.96"
|
||||
android:endX="102.3"
|
||||
android:endY="165.06"
|
||||
android:type="linear">
|
||||
<item android:offset="0.48" android:color="#FFFFFFFF"/>
|
||||
<item android:offset="0.63" android:color="#FFF5E4C2"/>
|
||||
<item android:offset="0.73" android:color="#FFE5C7AD"/>
|
||||
<item android:offset="1" android:color="#FFB790A9"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
@@ -0,0 +1,42 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="204dp"
|
||||
android:height="205dp"
|
||||
android:viewportWidth="204"
|
||||
android:viewportHeight="205">
|
||||
<path
|
||||
android:pathData="M114.92,6.98L109,2.47C104.7,-0.82 98.72,-0.82 94.42,2.47L4.72,70.96C0.72,74.02 -0.95,79.25 0.54,84.06L35.03,195.65C36.59,200.69 41.24,204.12 46.51,204.12H156.91C162.18,204.12 166.83,200.69 168.38,195.65L202.88,84.06C204.37,79.25 202.7,74.02 198.7,70.96L190.43,64.65L180.92,94.56L154.79,179.53C153.77,182.85 150.71,185.12 147.23,185.12H55.6C52.13,185.12 49.06,182.85 48.04,179.53L19.44,86.54C18.47,83.38 19.57,79.95 22.19,77.94L96.6,20.81C96.9,20.58 97.21,20.38 97.53,20.19L114.92,6.98Z"
|
||||
android:fillType="evenOdd">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="65.53"
|
||||
android:startY="18.28"
|
||||
android:endX="150.22"
|
||||
android:endY="187.89"
|
||||
android:type="linear">
|
||||
<item android:offset="0.0" android:color="#fff5b11d"/>
|
||||
<item android:offset="0.10" android:color="#fff0834d"/>
|
||||
<item android:offset="0.24" android:color="#ffe85f6b"/>
|
||||
<item android:offset="0.49" android:color="#ffc9659e"/>
|
||||
<item android:offset="1" android:color="#ffb9588f"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M98.37,41.8C100.69,40.03 103.9,40.03 106.22,41.8L161.71,84.35C163.85,85.98 164.74,88.78 163.95,91.36L142.61,160.64C141.78,163.35 139.28,165.19 136.45,165.19H68.14C65.31,165.19 62.81,163.35 61.98,160.64L58.33,148.8H76.42C76.63,148.84 76.85,148.87 77.07,148.87H126.35C127.77,148.87 129.03,147.93 129.44,146.57L144.82,95.62C145.2,94.35 144.76,92.97 143.72,92.15L103.69,60.85C102.53,59.94 100.89,59.94 99.72,60.85L63.01,89.56L44.33,103.33L40.65,91.36C39.85,88.78 40.75,85.98 42.88,84.35L98.37,41.8Z"
|
||||
android:fillType="evenOdd">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="102.3"
|
||||
android:startY="-83.96"
|
||||
android:endX="102.3"
|
||||
android:endY="165.06"
|
||||
android:type="linear">
|
||||
<item android:offset="0.48" android:color="#fff2ce00"/>
|
||||
<item android:offset="0.63" android:color="#fff4b97e"/>
|
||||
<item android:offset="0.73" android:color="#fff7b66a"/>
|
||||
<item android:offset="1" android:color="#ffeb8fac"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
@@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="96dp"
|
||||
android:height="96dp"
|
||||
android:viewportWidth="696"
|
||||
android:viewportHeight="698"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:pathData="M336.61,143.26C344.51,137.21 355.49,137.21 363.39,143.26L552.8,288.49C560.1,294.09 563.14,303.63 560.44,312.42L487.62,548.92C484.78,558.15 476.25,564.45 466.59,564.45H233.41C223.75,564.45 215.22,558.15 212.38,548.92L199.93,508.5H261.68C262.39,508.65 263.13,508.72 263.89,508.72H432.11C436.96,508.72 441.24,505.55 442.64,500.9L495.14,326.98C496.45,322.64 494.96,317.93 491.39,315.14L354.78,208.3C350.8,205.19 345.2,205.19 341.22,208.3L215.93,306.29L152.15,353.31L139.56,312.42C136.86,303.63 139.9,294.09 147.2,288.49L336.61,143.26Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="@android:color/white">
|
||||
</path>
|
||||
</vector>
|
||||
@@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="96dp"
|
||||
android:height="96dp"
|
||||
android:viewportWidth="696"
|
||||
android:viewportHeight="698"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:pathData="M393.08,24.42L372.88,9C358.19,-2.22 337.81,-2.22 323.12,9L16.94,242.81C3.28,253.24 -2.42,271.09 2.66,287.51L120.41,668.43C125.72,685.61 141.6,697.33 159.58,697.33H536.42C554.4,697.33 570.28,685.61 575.59,668.43L693.34,287.51C698.42,271.09 692.72,253.24 679.06,242.81L650.84,221.26L618.37,323.39L529.2,613.39C525.71,624.72 515.24,632.46 503.39,632.46H190.61C178.76,632.46 168.29,624.72 164.8,613.39L67.2,295.97C63.88,285.19 67.62,273.49 76.56,266.62L330.56,71.62C331.58,70.84 332.64,70.14 333.74,69.52L393.08,24.42Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="@android:color/white">
|
||||
</path>
|
||||
</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="M440,520L200,520L200,440L440,440L440,200L520,200L520,440L760,440L760,520L520,520L520,760L440,760L440,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="M313,520L537,744L480,800L160,480L480,160L537,216L313,440L800,440L800,520L313,520Z"/>
|
||||
</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="M382,720L154,492L211,435L382,606L749,239L806,296L382,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="M336,680L280,624L424,480L280,337L336,281L480,425L623,281L679,337L535,480L679,624L623,680L480,536L336,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="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,680L520,680L520,440L440,440L440,680ZM480,360Q497,360 508.5,348.5Q520,337 520,320Q520,303 508.5,291.5Q497,280 480,280Q463,280 451.5,291.5Q440,303 440,320Q440,337 451.5,348.5Q463,360 480,360ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||
</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,800Q447,800 423.5,776.5Q400,753 400,720Q400,687 423.5,663.5Q447,640 480,640Q513,640 536.5,663.5Q560,687 560,720Q560,753 536.5,776.5Q513,800 480,800ZM480,560Q447,560 423.5,536.5Q400,513 400,480Q400,447 423.5,423.5Q447,400 480,400Q513,400 536.5,423.5Q560,447 560,480Q560,513 536.5,536.5Q513,560 480,560ZM480,320Q447,320 423.5,296.5Q400,273 400,240Q400,207 423.5,183.5Q447,160 480,160Q513,160 536.5,183.5Q560,207 560,240Q560,273 536.5,296.5Q513,320 480,320Z"/>
|
||||
</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="M320,760L320,200L760,480L320,760ZM400,480L400,480L400,480ZM400,614L610,480L400,346L400,614Z"/>
|
||||
</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,800Q346,800 253,707Q160,614 160,480Q160,346 253,253Q346,160 480,160Q549,160 612,188.5Q675,217 720,270L720,160L800,160L800,440L520,440L520,360L688,360Q656,304 600.5,272Q545,240 480,240Q380,240 310,310Q240,380 240,480Q240,580 310,650Q380,720 480,720Q557,720 619,676Q681,632 706,560L790,560Q762,666 676,733Q590,800 480,800Z"/>
|
||||
</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="M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM440,800L519,800L533,694Q564,686 590.5,670.5Q617,655 639,633L738,674L777,606L691,541Q696,527 698,511.5Q700,496 700,480Q700,464 698,448.5Q696,433 691,419L777,354L738,286L639,328Q617,305 590.5,289.5Q564,274 533,266L520,160L441,160L427,266Q396,274 369.5,289.5Q343,305 321,327L222,286L183,354L269,418Q264,433 262,448Q260,463 260,480Q260,496 262,511Q264,526 269,541L183,606L222,674L321,632Q343,655 369.5,670.5Q396,686 427,694L440,800ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620ZM480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Z"/>
|
||||
</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="M680,880Q630,880 595,845Q560,810 560,760Q560,754 563,732L282,568Q266,583 245,591.5Q224,600 200,600Q150,600 115,565Q80,530 80,480Q80,430 115,395Q150,360 200,360Q224,360 245,368.5Q266,377 282,392L563,228Q561,221 560.5,214.5Q560,208 560,200Q560,150 595,115Q630,80 680,80Q730,80 765,115Q800,150 800,200Q800,250 765,285Q730,320 680,320Q656,320 635,311.5Q614,303 598,288L317,452Q319,459 319.5,465.5Q320,472 320,480Q320,488 319.5,494.5Q319,501 317,508L598,672Q614,657 635,648.5Q656,640 680,640Q730,640 765,675Q800,710 800,760Q800,810 765,845Q730,880 680,880ZM680,800Q697,800 708.5,788.5Q720,777 720,760Q720,743 708.5,731.5Q697,720 680,720Q663,720 651.5,731.5Q640,743 640,760Q640,777 651.5,788.5Q663,800 680,800ZM200,520Q217,520 228.5,508.5Q240,497 240,480Q240,463 228.5,451.5Q217,440 200,440Q183,440 171.5,451.5Q160,463 160,480Q160,497 171.5,508.5Q183,520 200,520ZM680,240Q697,240 708.5,228.5Q720,217 720,200Q720,183 708.5,171.5Q697,160 680,160Q663,160 651.5,171.5Q640,183 640,200Q640,217 651.5,228.5Q663,240 680,240ZM680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760ZM200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480ZM680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Z"/>
|
||||
</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,880Q341,845 250.5,720.5Q160,596 160,444L160,200L480,80L800,200L800,444Q800,596 709.5,720.5Q619,845 480,880ZM480,796Q584,763 652,664Q720,565 720,444L720,255L480,165L240,255L240,444Q240,565 308,664Q376,763 480,796ZM480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||
</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,640Q497,640 508.5,628.5Q520,617 520,600Q520,583 508.5,571.5Q497,560 480,560Q463,560 451.5,571.5Q440,583 440,600Q440,617 451.5,628.5Q463,640 480,640ZM440,480L520,480L520,280L440,280L440,480ZM480,880Q341,845 250.5,720.5Q160,596 160,444L160,200L480,80L800,200L800,444Q800,596 709.5,720.5Q619,845 480,880ZM480,796Q584,763 652,664Q720,565 720,444L720,255L480,165L240,255L240,444Q240,565 308,664Q376,763 480,796ZM480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||
</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,840Q405,840 339.5,811.5Q274,783 225.5,734.5Q177,686 148.5,620.5Q120,555 120,480Q120,405 148.5,339.5Q177,274 225.5,225.5Q274,177 339.5,148.5Q405,120 480,120Q562,120 635.5,155Q709,190 760,254L760,160L840,160L840,400L600,400L600,320L710,320Q669,264 609,232Q549,200 480,200Q363,200 281.5,281.5Q200,363 200,480Q200,597 281.5,678.5Q363,760 480,760Q585,760 663.5,692Q742,624 756,520L838,520Q823,657 720.5,748.5Q618,840 480,840ZM592,648L440,496L440,280L520,280L520,464L648,592L592,648Z"/>
|
||||
</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="M40,840L480,80L920,840L40,840ZM178,760L782,760L480,240L178,760ZM480,720Q497,720 508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720ZM440,600L520,600L520,400L440,400L440,600ZM480,500L480,500L480,500Z"/>
|
||||
</vector>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||
</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_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>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</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>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 25 KiB |