mirror of
https://github.com/geode-sdk/android-launcher.git
synced 2026-10-22 16:42:38 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
915e232011 | ||
|
|
cf4cd8335e |
No files matched your search
@@ -1,18 +1,3 @@
|
|||||||
# Geode Android Launcher
|
# Geode Android Launcher
|
||||||
|
|
||||||
Launches a vanilla copy of Geometry Dash with the Geode loader added.
|
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 |
|
|
||||||
+23
-19
@@ -1,3 +1,5 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
val composeBOM: String by rootProject.extra
|
val composeBOM: String by rootProject.extra
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
@@ -8,6 +10,7 @@ kotlin {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
id("org.jetbrains.kotlin.plugin.serialization")
|
id("org.jetbrains.kotlin.plugin.serialization")
|
||||||
id("org.jetbrains.kotlin.plugin.compose")
|
id("org.jetbrains.kotlin.plugin.compose")
|
||||||
}
|
}
|
||||||
@@ -18,9 +21,9 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.geode.launcher"
|
applicationId = "com.geode.launcher"
|
||||||
minSdk = 23
|
minSdk = 23
|
||||||
targetSdk = 36
|
targetSdk = 35
|
||||||
versionCode = 24
|
versionCode = 22
|
||||||
versionName = "1.7.1"
|
versionName = "1.6.1"
|
||||||
|
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
@@ -29,7 +32,7 @@ android {
|
|||||||
@Suppress("UnstableApiUsage")
|
@Suppress("UnstableApiUsage")
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
arguments("-DUSE_TULIPHOOK:BOOL=OFF", "-DANDROID_STL=c++_shared")
|
arguments("-DUSE_TULIPHOOK:BOOL=OFF", "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES:BOOL=OFF")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +70,11 @@ android {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = JvmTarget.JVM_1_8
|
||||||
|
}
|
||||||
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
@@ -83,7 +91,7 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
namespace = "com.geode.launcher"
|
namespace = "com.geode.launcher"
|
||||||
ndkVersion = "29.0.14206865"
|
ndkVersion = "28.1.13356709"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -92,23 +100,19 @@ dependencies {
|
|||||||
implementation ("androidx.compose.ui:ui")
|
implementation ("androidx.compose.ui:ui")
|
||||||
implementation ("androidx.compose.material3:material3")
|
implementation ("androidx.compose.material3:material3")
|
||||||
implementation ("androidx.compose.ui:ui-tooling-preview")
|
implementation ("androidx.compose.ui:ui-tooling-preview")
|
||||||
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
|
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.9.3")
|
||||||
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
|
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.3")
|
||||||
implementation ("androidx.activity:activity-compose:1.12.4")
|
implementation ("androidx.activity:activity-compose:1.11.0")
|
||||||
implementation ("androidx.activity:activity-ktx:1.12.4")
|
implementation ("androidx.activity:activity-ktx:1.11.0")
|
||||||
implementation ("androidx.appcompat:appcompat:1.7.1")
|
implementation ("androidx.appcompat:appcompat:1.7.1")
|
||||||
implementation ("androidx.documentfile:documentfile:1.1.0")
|
implementation ("androidx.documentfile:documentfile:1.1.0")
|
||||||
|
implementation ("com.squareup.okio:okio:3.16.0")
|
||||||
implementation ("com.squareup.okio:okio:3.16.4")
|
implementation ("com.squareup.okhttp3:okhttp:5.1.0")
|
||||||
implementation ("com.squareup.okhttp3:okhttp:5.3.2")
|
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||||
implementation ("com.squareup.okhttp3:okhttp-coroutines:5.3.2")
|
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.9.0")
|
||||||
|
|
||||||
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 ("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
|
||||||
|
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.37.0")
|
||||||
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.39.2")
|
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.37.0")
|
||||||
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.39.2")
|
|
||||||
implementation ("androidx.browser:browser:1.9.0")
|
implementation ("androidx.browser:browser:1.9.0")
|
||||||
debugImplementation ("androidx.compose.ui:ui-tooling")
|
debugImplementation ("androidx.compose.ui:ui-tooling")
|
||||||
coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.1.5")
|
coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.1.5")
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
<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.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.LIGHTS" />
|
<uses-permission android:name="android.permission.LIGHTS" />
|
||||||
|
|
||||||
@@ -39,57 +38,33 @@
|
|||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:appCategory="game"
|
android:appCategory="game"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:hasFragileUserData="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:hasFragileUserData="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:theme="@style/Theme.GeodeLauncher"
|
android:theme="@style/Theme.GeodeLauncher"
|
||||||
tools:targetApi="35">
|
tools:targetApi="35">
|
||||||
<receiver
|
<activity
|
||||||
android:name=".InstallReceiver"
|
android:name=".AltMainActivity"
|
||||||
android:enabled="true"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:theme="@style/Theme.GeodeLauncher" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".preferences.DeveloperSettingsActivity"
|
android:name=".preferences.DeveloperSettingsActivity"
|
||||||
android:exported="true"
|
android:exported="false"
|
||||||
android:label="@string/title_activity_developer_settings"
|
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
|
<activity
|
||||||
android:name=".preferences.ApplicationLogsActivity"
|
android:name=".preferences.ApplicationLogsActivity"
|
||||||
android:exported="true"
|
android:exported="false"
|
||||||
android:label="@string/title_activity_application_logs"
|
android:label="@string/title_activity_application_logs"
|
||||||
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="logs" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".preferences.CrashLogsActivity"
|
android:name=".preferences.CrashLogsActivity"
|
||||||
android:exported="true"
|
android:exported="false"
|
||||||
android:label="@string/title_activity_crash_logs"
|
android:label="@string/title_activity_crash_logs"
|
||||||
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="crashes" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".preferences.ApplicationIconActivity"
|
android:name=".preferences.ApplicationIconActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
@@ -97,24 +72,17 @@
|
|||||||
android:theme="@style/Theme.GeodeLauncher" />
|
android:theme="@style/Theme.GeodeLauncher" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".preferences.SettingsActivity"
|
android:name=".preferences.SettingsActivity"
|
||||||
android:exported="true"
|
android:exported="false"
|
||||||
android:label="@string/title_activity_settings"
|
android:label="@string/title_activity_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="settings" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".GeometryDashActivity"
|
android:name=".GeometryDashActivity"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize"
|
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:screenOrientation="landscape"
|
android:screenOrientation="landscape"
|
||||||
android:theme="@style/Theme.NoActionBar.Fullscreen"
|
android:theme="@style/Theme.NoActionBar.Fullscreen"
|
||||||
|
android:configChanges="orientation|screenSize|smallestScreenSize"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden">
|
android:windowSoftInputMode="stateAlwaysHidden">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.lib_name"
|
android:name="android.app.lib_name"
|
||||||
@@ -126,77 +94,47 @@
|
|||||||
android:theme="@style/Theme.GeodeLauncher">
|
android:theme="@style/Theme.GeodeLauncher">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</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>
|
||||||
|
|
||||||
|
<activity-alias
|
||||||
|
android:name=".MainActivityGeode"
|
||||||
|
android:enabled="false"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:icon="@mipmap/ic_launcher_geode"
|
||||||
|
android:targetActivity=".MainActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity-alias>
|
||||||
|
|
||||||
|
<activity-alias
|
||||||
|
android:name=".MainActivityPride"
|
||||||
|
android:enabled="false"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:icon="@mipmap/ic_launcher_pride"
|
||||||
|
android:targetActivity=".MainActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity-alias>
|
||||||
|
|
||||||
<activity-alias
|
<activity-alias
|
||||||
android:name=".MainActivityTrans"
|
android:name=".MainActivityTrans"
|
||||||
android:enabled="false"
|
android:enabled="false"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
android:icon="@mipmap/ic_launcher_trans"
|
android:icon="@mipmap/ic_launcher_trans"
|
||||||
android:label="@string/app_name"
|
|
||||||
android:targetActivity=".MainActivity">
|
android:targetActivity=".MainActivity">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</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>
|
</activity-alias>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
|
|||||||
@@ -261,8 +261,8 @@ int on_dl_iterate(dl_phdr_info* info, size_t size, void* data) {
|
|||||||
|
|
||||||
// this should be called after gd is loaded but before geode
|
// this should be called after gd is loaded but before geode
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_performExceptionsRenaming(JNIEnv*, jobject) {
|
JNIEXPORT int JNICALL Java_com_geode_launcher_LauncherFix_performExceptionsRenaming(JNIEnv*, jobject) {
|
||||||
dl_iterate_phdr(on_dl_iterate, nullptr);
|
return dl_iterate_phdr(on_dl_iterate, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::string> redirect_path(const char* pathname) {
|
std::optional<std::string> redirect_path(const char* pathname) {
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import android.app.Activity
|
|||||||
import android.content.*
|
import android.content.*
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
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.closeIMEKeyboard
|
||||||
import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.openIMEKeyboard
|
import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.openIMEKeyboard
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
@@ -61,40 +61,33 @@ object BaseRobTopActivity {
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun openURL(url: String) {
|
fun openURL(url: String) {
|
||||||
|
Log.d("MAIN", "Open URL")
|
||||||
me.get()?.runOnUiThread {
|
me.get()?.runOnUiThread {
|
||||||
try {
|
try {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
me.get()?.startActivity(
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
Intent(
|
||||||
me.get()?.startActivity(intent)
|
"android.intent.action.VIEW",
|
||||||
|
Uri.parse(url)
|
||||||
|
)
|
||||||
|
)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
me.get(),
|
me.get(),
|
||||||
R.string.no_activity_found,
|
"No activity found to open this URL.",
|
||||||
Toast.LENGTH_SHORT,
|
Toast.LENGTH_SHORT,
|
||||||
).show()
|
).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
|
@JvmStatic
|
||||||
fun sendMail(subject: String, body: String, to: String) {
|
fun sendMail(subject: String, body: String, to: String) {
|
||||||
me.get()?.runOnUiThread {
|
me.get()?.runOnUiThread {
|
||||||
val i = Intent(Intent.ACTION_SEND)
|
val i = Intent("android.intent.action.SEND")
|
||||||
i.type = "message/rfc822"
|
i.type = "message/rfc822"
|
||||||
i.putExtra(Intent.EXTRA_EMAIL, arrayOf(to))
|
i.putExtra("android.intent.extra.EMAIL", arrayOf(to))
|
||||||
i.putExtra(Intent.EXTRA_SUBJECT, subject)
|
i.putExtra("android.intent.extra.SUBJECT", subject)
|
||||||
i.putExtra(Intent.EXTRA_TEXT, body)
|
i.putExtra("android.intent.extra.TEXT", body)
|
||||||
try {
|
try {
|
||||||
me.get()?.startActivity(Intent.createChooser(i, "Send mail..."))
|
me.get()?.startActivity(Intent.createChooser(i, "Send mail..."))
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
|||||||
@@ -0,0 +1,529 @@
|
|||||||
|
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.offset
|
||||||
|
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.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.geode.launcher.main.*
|
||||||
|
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||||
|
import com.geode.launcher.ui.theme.LocalTheme
|
||||||
|
import com.geode.launcher.ui.theme.Theme
|
||||||
|
import com.geode.launcher.ui.theme.launcherTitleStyle
|
||||||
|
import com.geode.launcher.utils.Constants
|
||||||
|
import com.geode.launcher.utils.GamePackageUtils
|
||||||
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
|
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 dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||||
|
|
||||||
|
val launchViewModel = viewModel<LaunchViewModel>(factory = LaunchViewModel.Factory)
|
||||||
|
|
||||||
|
if (loadFailureInfo != null) {
|
||||||
|
launchViewModel.loadFailure = loadFailureInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalTheme provides theme) {
|
||||||
|
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.geode_monochrome),
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,7 +12,6 @@ import android.os.Environment
|
|||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowInsets
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
@@ -23,7 +22,6 @@ import androidx.compose.ui.platform.ComposeView
|
|||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import com.customRobTop.BaseRobTopActivity
|
import com.customRobTop.BaseRobTopActivity
|
||||||
import com.customRobTop.JniToCpp
|
import com.customRobTop.JniToCpp
|
||||||
import com.geode.launcher.main.LaunchNotification
|
import com.geode.launcher.main.LaunchNotification
|
||||||
@@ -187,60 +185,16 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
|
|
||||||
loadInternalMods()
|
loadInternalMods()
|
||||||
|
|
||||||
val baseView = createView()
|
setContentView(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)
|
setupPostLibraryLoad(gdPackageInfo)
|
||||||
|
|
||||||
val geodeFailed = try {
|
try {
|
||||||
loadGeodeLibrary()
|
loadGeodeLibrary()
|
||||||
false
|
|
||||||
} catch (e: UnsatisfiedLinkError) {
|
} catch (e: UnsatisfiedLinkError) {
|
||||||
handleGeodeException(e)
|
handleGeodeException(e)
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
handleGeodeException(e)
|
handleGeodeException(e)
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
setupNotificationView(baseView, geodeFailed)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupNotificationView(baseView: FrameLayout, geodeFailed: Boolean) {
|
|
||||||
val cards = determineDisplayedCards(this, geodeFailed)
|
|
||||||
if (cards.isNotEmpty()) {
|
|
||||||
val notificationView = ComposeView(this)
|
|
||||||
baseView.addView(notificationView)
|
|
||||||
|
|
||||||
notificationView.setContent {
|
|
||||||
LaunchNotification(cards)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,6 +415,19 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
this.mGLSurfaceView = glSurfaceView
|
this.mGLSurfaceView = glSurfaceView
|
||||||
frameLayout.addView(this.mGLSurfaceView)
|
frameLayout.addView(this.mGLSurfaceView)
|
||||||
|
|
||||||
|
val showNotification = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.ENABLE_REDESIGN)
|
||||||
|
if (showNotification) {
|
||||||
|
val hasCards = determineDisplayedCards(this)
|
||||||
|
if (hasCards.isNotEmpty()) {
|
||||||
|
val notificationView = ComposeView(this)
|
||||||
|
frameLayout.addView(notificationView)
|
||||||
|
|
||||||
|
notificationView.setContent {
|
||||||
|
LaunchNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
glSurfaceView.setEGLContextClientVersion(2)
|
glSurfaceView.setEGLContextClientVersion(2)
|
||||||
glSurfaceView.setEGLConfigChooser(EGLConfigChooser())
|
glSurfaceView.setEGLConfigChooser(EGLConfigChooser())
|
||||||
|
|
||||||
@@ -498,11 +465,11 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
val limitedRefreshRate = preferenceUtils.getInt(PreferenceUtils.Key.LIMIT_FRAME_RATE).takeIf { it != 0 }
|
val limitedRefreshRate = preferenceUtils.getInt(PreferenceUtils.Key.LIMIT_FRAME_RATE).takeIf { it != 0 }
|
||||||
mLimitedRefreshRate = limitedRefreshRate
|
mLimitedRefreshRate = limitedRefreshRate
|
||||||
|
|
||||||
mForceRefreshRate = true
|
mForceRefreshRate = preferenceUtils.getBoolean(PreferenceUtils.Key.FORCE_HRR)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (mForceRefreshRate && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
val chosenDisplay = display.supportedModes?.maxByOrNull { it.refreshRate }
|
val chosenDisplay = display.supportedModes?.maxByOrNull { it.refreshRate }
|
||||||
if (chosenDisplay != null) {
|
if (chosenDisplay != null) {
|
||||||
val chosenRefreshRate = chosenDisplay.refreshRate
|
var chosenRefreshRate = chosenDisplay.refreshRate
|
||||||
println("setupUIState: Forcing a refresh rate of $chosenRefreshRate (display ${chosenDisplay.modeId})")
|
println("setupUIState: Forcing a refresh rate of $chosenRefreshRate (display ${chosenDisplay.modeId})")
|
||||||
|
|
||||||
window.attributes.preferredRefreshRate = chosenRefreshRate
|
window.attributes.preferredRefreshRate = chosenRefreshRate
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
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,52 +1,17 @@
|
|||||||
package com.geode.launcher
|
package com.geode.launcher
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.format.Formatter
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
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.Image
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.layout.*
|
||||||
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.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.*
|
||||||
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.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -54,58 +19,37 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalResources
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.geode.launcher.main.*
|
import com.geode.launcher.main.*
|
||||||
|
import com.geode.launcher.updater.ReleaseViewModel
|
||||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||||
import com.geode.launcher.ui.theme.LocalTheme
|
import com.geode.launcher.ui.theme.LocalTheme
|
||||||
import com.geode.launcher.ui.theme.Theme
|
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.ui.theme.launcherTitleStyle
|
||||||
import com.geode.launcher.updater.ReleaseManager
|
|
||||||
import com.geode.launcher.utils.Constants
|
import com.geode.launcher.utils.Constants
|
||||||
import com.geode.launcher.utils.GamePackageUtils
|
|
||||||
import com.geode.launcher.utils.LaunchUtils
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
import com.geode.launcher.utils.PreferenceUtils
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
import kotlinx.coroutines.CancellationException
|
import com.geode.launcher.utils.GamePackageUtils
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.net.ConnectException
|
|
||||||
import java.net.UnknownHostException
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
enableEdgeToEdge()
|
||||||
|
clearDownloadedApks(this)
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val gdInstalled = GamePackageUtils.isGameInstalled(packageManager)
|
val gdInstalled = GamePackageUtils.isGameInstalled(packageManager)
|
||||||
val geodeInstalled = LaunchUtils.isGeodeInstalled(this)
|
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 returnMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE)
|
||||||
val returnExtendedMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE)
|
val returnExtendedMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE)
|
||||||
|
|
||||||
@@ -119,19 +63,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
LoadFailureInfo(LaunchUtils.LauncherError.CRASHED)
|
LoadFailureInfo(LaunchUtils.LauncherError.CRASHED)
|
||||||
} else { null }
|
} else { null }
|
||||||
|
|
||||||
var launchArguments: LaunchArguments? = null
|
val redirectToAlt = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.ENABLE_REDESIGN)
|
||||||
val intentData = intent.data
|
if (redirectToAlt) {
|
||||||
if (intent.action == Intent.ACTION_VIEW && intentData != null) {
|
val launchIntent = Intent(this, AltMainActivity::class.java)
|
||||||
val isLaunch = intentData.path == "/launch"
|
launchIntent.putExtras(intent)
|
||||||
val safeMode = if (isLaunch) {
|
startActivity(launchIntent)
|
||||||
intentData.getBooleanQueryParameter("safe-mode", false)
|
|
||||||
} else false
|
|
||||||
|
|
||||||
launchArguments = LaunchArguments(
|
return
|
||||||
autoSafeMode = safeMode,
|
|
||||||
forcePause = !isLaunch,
|
|
||||||
forceLaunch = isLaunch,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
@@ -141,631 +79,195 @@ class MainActivity : ComponentActivity() {
|
|||||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
launchViewModel.beginLaunchFlow()
|
|
||||||
|
|
||||||
CompositionLocalProvider(LocalTheme provides theme) {
|
CompositionLocalProvider(LocalTheme provides theme) {
|
||||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
color = MaterialTheme.colorScheme.background
|
||||||
) {
|
) {
|
||||||
AltMainScreen(launchViewModel)
|
MainScreen(gdInstalled, geodeInstalled, loadFailureInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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.LAST_LAUNCH_CRASHED_AUTOMATIC,
|
|
||||||
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
|
@Composable
|
||||||
fun mapCancelReasonToInfo(cancelReason: LaunchViewModel.LaunchCancelReason): LaunchStatusInfo {
|
fun MainScreen(
|
||||||
return when (cancelReason) {
|
gdInstalled: Boolean = true,
|
||||||
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> LaunchStatusInfo(
|
geodePreinstalled: Boolean = true,
|
||||||
title = stringResource(R.string.launcher_cancelled_crash)
|
loadFailureInfo: LoadFailureInfo? = null,
|
||||||
)
|
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
|
||||||
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED_AUTOMATIC -> LaunchStatusInfo(
|
) {
|
||||||
title = stringResource(R.string.launcher_cancelled_crash_instant)
|
|
||||||
)
|
|
||||||
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 context = LocalContext.current
|
||||||
|
|
||||||
val downloaded = remember(state.downloaded) {
|
val shouldUpdate by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
|
||||||
Formatter.formatShortFileSize(context, state.downloaded)
|
val autoUpdateState by releaseViewModel.uiState.collectAsState()
|
||||||
}
|
|
||||||
|
|
||||||
val outOf = remember(state.outOf) {
|
val geodeJustInstalled = (autoUpdateState as? ReleaseViewModel.ReleaseUIState.Finished)
|
||||||
state.outOf?.run {
|
?.hasUpdated == true
|
||||||
Formatter.formatShortFileSize(context, this)
|
val geodeInstalled = geodePreinstalled || geodeJustInstalled
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchStatusInfo(
|
var beginLaunch by remember { mutableStateOf(false) }
|
||||||
title = stringResource(R.string.launcher_downloading_update),
|
var launchInSafeMode by remember { mutableStateOf(false) }
|
||||||
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
|
LaunchedEffect(shouldUpdate) {
|
||||||
fun LaunchCancelledBody(statusInfo: LaunchStatusInfo, icon: @Composable () -> Unit, inProgress: Boolean, modifier: Modifier = Modifier) {
|
if (shouldUpdate && !releaseViewModel.hasPerformedCheck && gdInstalled) {
|
||||||
if (statusInfo.title == null && statusInfo.details == null && !inProgress) {
|
releaseViewModel.runReleaseCheck()
|
||||||
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 {
|
} else {
|
||||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
releaseViewModel.useGlobalCheckState()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusInfo.details != null) {
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
Text(statusInfo.details, style = MaterialTheme.typography.bodyMedium)
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
if (showErrorInfo && loadFailureInfo != null) {
|
||||||
fun RetryButtonContents(reason: LaunchViewModel.LaunchCancelReason) {
|
ErrorInfoSheet(loadFailureInfo, onDismiss = { showErrorInfo = false })
|
||||||
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.LAST_LAUNCH_CRASHED_AUTOMATIC,
|
|
||||||
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)
|
Scaffold(
|
||||||
@Composable
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||||
fun LaunchProgressCard(
|
) { innerPadding ->
|
||||||
uiState: LaunchViewModel.LaunchUIState,
|
Column(
|
||||||
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 AnimatedLogo(modifier: Modifier = Modifier) {
|
|
||||||
val theme = LocalTheme.current
|
|
||||||
val innerColorPalette = remember(theme) {
|
|
||||||
if (theme == LIGHT) lightInnerColorGradient else darkInnerColorGradient
|
|
||||||
}
|
|
||||||
|
|
||||||
val outerColorPalette = remember(theme) {
|
|
||||||
if (theme == LIGHT) lightOuterColorGradient else darkOuterColorGradient
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
modifier = Modifier
|
||||||
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
.padding(innerPadding)
|
||||||
.drawWithContent {
|
.fillMaxSize()
|
||||||
drawContent()
|
.verticalScroll(rememberScrollState()),
|
||||||
drawRect(
|
verticalArrangement = Arrangement.Center,
|
||||||
brush = innerBrush,
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
blendMode = BlendMode.SrcIn
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.drawable.geode_monochrome_outer),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier
|
|
||||||
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
|
||||||
.drawWithContent {
|
|
||||||
drawContent()
|
|
||||||
drawRect(
|
|
||||||
brush = outerBrush,
|
|
||||||
blendMode = BlendMode.SrcIn
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun GeodeLogo(shouldAnimate: Boolean = false, modifier: Modifier = Modifier) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = modifier
|
|
||||||
) {
|
) {
|
||||||
val theme = LocalTheme.current
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.geode_logo),
|
||||||
Crossfade(
|
contentDescription = context.getString(R.string.launcher_logo_alt),
|
||||||
targetState = shouldAnimate,
|
modifier = Modifier.size(100.dp, 100.dp)
|
||||||
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(
|
Text(
|
||||||
stringResource(R.string.launcher_title),
|
context.getString(R.string.launcher_title),
|
||||||
style = launcherTitleStyle,
|
style = launcherTitleStyle,
|
||||||
fontSize = 64.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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> LaunchBlockedLabel(stringResource(R.string.game_not_found))
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateCard(
|
||||||
|
releaseViewModel,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(12.dp)
|
.padding(12.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
if (beginLaunch) {
|
||||||
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
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (launchUIState is LaunchViewModel.LaunchUIState.Ready) {
|
|
||||||
UpdateWarning(launchInSafeMode) {
|
UpdateWarning(launchInSafeMode) {
|
||||||
|
beginLaunch = false
|
||||||
launchInSafeMode = false
|
launchInSafeMode = false
|
||||||
coroutineScope.launch {
|
|
||||||
launchViewModel.cancelLaunch()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val loadFailure = launchViewModel.currentCrashInfo()
|
@Preview(showSystemUi = true)
|
||||||
if (showErrorInfo && loadFailure != null) {
|
@Composable
|
||||||
ErrorInfoSheet(loadFailure, onDismiss = { showErrorInfo = false })
|
fun MainScreenLightPreview() {
|
||||||
|
GeodeLauncherTheme {
|
||||||
|
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||||
|
MainScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,8 +45,7 @@ class CrashViewModel(private val application: Application) : ViewModel() {
|
|||||||
|
|
||||||
private var logJob: Job? = null
|
private var logJob: Job? = null
|
||||||
|
|
||||||
var hasIndicator = false
|
@OptIn(ExperimentalTime::class)
|
||||||
|
|
||||||
private fun getCrashList(): List<CrashDump> {
|
private fun getCrashList(): List<CrashDump> {
|
||||||
val crashDirectory = LaunchUtils.getCrashDirectory(application)
|
val crashDirectory = LaunchUtils.getCrashDirectory(application)
|
||||||
if (!crashDirectory.exists()) {
|
if (!crashDirectory.exists()) {
|
||||||
@@ -58,8 +57,6 @@ class CrashViewModel(private val application: Application) : ViewModel() {
|
|||||||
_, name -> name != LaunchUtils.CRASH_INDICATOR_NAME && name != "last-pid"
|
_, name -> name != LaunchUtils.CRASH_INDICATOR_NAME && name != "last-pid"
|
||||||
} ?: return emptyList()
|
} ?: return emptyList()
|
||||||
|
|
||||||
hasIndicator = File(crashDirectory, LaunchUtils.CRASH_INDICATOR_NAME).exists()
|
|
||||||
|
|
||||||
return children
|
return children
|
||||||
.map {
|
.map {
|
||||||
CrashDump(
|
CrashDump(
|
||||||
@@ -70,24 +67,6 @@ class CrashViewModel(private val application: Application) : ViewModel() {
|
|||||||
.sortedByDescending { it.lastModified }
|
.sortedByDescending { it.lastModified }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearIndicator() {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
val crashDirectory = LaunchUtils.getCrashDirectory(application)
|
|
||||||
File(crashDirectory, LaunchUtils.CRASH_INDICATOR_NAME).delete()
|
|
||||||
|
|
||||||
hasIndicator = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createIndicator() {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
val crashDirectory = LaunchUtils.getCrashDirectory(application)
|
|
||||||
File(crashDirectory, LaunchUtils.CRASH_INDICATOR_NAME).createNewFile()
|
|
||||||
|
|
||||||
hasIndicator = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearCrashes() {
|
fun clearCrashes() {
|
||||||
logJob?.cancel()
|
logJob?.cancel()
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ package com.geode.launcher.main
|
|||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.AbsoluteRoundedCornerShape
|
import androidx.compose.foundation.shape.AbsoluteRoundedCornerShape
|
||||||
@@ -24,13 +27,12 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.geode.launcher.R
|
import com.geode.launcher.R
|
||||||
import com.geode.launcher.utils.Constants
|
import com.geode.launcher.utils.Constants
|
||||||
import androidx.core.net.toUri
|
|
||||||
|
|
||||||
fun onDownloadGame(context: Context) {
|
fun onDownloadGame(context: Context) {
|
||||||
val appUrl = "https://play.google.com/store/apps/details?id=${Constants.PACKAGE_NAME}"
|
val appUrl = "https://play.google.com/store/apps/details?id=${Constants.PACKAGE_NAME}"
|
||||||
try {
|
try {
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
data = appUrl.toUri()
|
data = Uri.parse(appUrl)
|
||||||
setPackage("com.android.vending")
|
setPackage("com.android.vending")
|
||||||
}
|
}
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
@@ -53,6 +55,22 @@ 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
|
@Composable
|
||||||
fun InlineDialog(headline: @Composable () -> Unit, body: @Composable () -> Unit, modifier: Modifier = Modifier) {
|
fun InlineDialog(headline: @Composable () -> Unit, body: @Composable () -> Unit, modifier: Modifier = Modifier) {
|
||||||
ElevatedCard(modifier = modifier) {
|
ElevatedCard(modifier = modifier) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.geode.launcher.main
|
package com.geode.launcher.main
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -19,6 +18,8 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
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.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -33,14 +34,10 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalResources
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
@@ -60,7 +57,6 @@ import com.geode.launcher.ui.theme.Typography
|
|||||||
import com.geode.launcher.utils.GamePackageUtils
|
import com.geode.launcher.utils.GamePackageUtils
|
||||||
import com.geode.launcher.utils.LaunchUtils
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
import com.geode.launcher.utils.PreferenceUtils
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
data class LoadFailureInfo(
|
data class LoadFailureInfo(
|
||||||
val title: LaunchUtils.LauncherError,
|
val title: LaunchUtils.LauncherError,
|
||||||
@@ -213,12 +209,9 @@ fun ErrorInfoDescription(description: String, modifier: Modifier = Modifier) {
|
|||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
|
fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
|
||||||
|
val clipboardManager = LocalClipboardManager.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val clipboard = LocalClipboard.current
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
val resources = LocalResources.current
|
|
||||||
|
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.spacedBy(
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
@@ -236,15 +229,7 @@ fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FilledTonalButton(onClick = {
|
FilledTonalButton(onClick = {
|
||||||
val clipboardLabel = resources.getString(R.string.application_log_copy_label)
|
clipboardManager.setText(AnnotatedString(extraDetails))
|
||||||
|
|
||||||
coroutineScope.launch {
|
|
||||||
clipboard.setClipEntry(
|
|
||||||
ClipEntry(ClipData.newPlainText(
|
|
||||||
clipboardLabel, extraDetails
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_content_copy),
|
painterResource(R.drawable.icon_content_copy),
|
||||||
@@ -256,7 +241,7 @@ fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
|
|||||||
} else {
|
} else {
|
||||||
Button(onClick = { onShareCrash(context) }) {
|
Button(onClick = { onShareCrash(context) }) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_share),
|
Icons.Filled.Share,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
|||||||
@@ -4,12 +4,21 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.PressInteraction
|
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.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
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.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -19,21 +28,25 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.geode.launcher.GeometryDashActivity
|
import com.geode.launcher.GeometryDashActivity
|
||||||
import com.geode.launcher.R
|
import com.geode.launcher.R
|
||||||
import com.geode.launcher.preferences.SettingsActivity
|
import com.geode.launcher.preferences.SettingsActivity
|
||||||
|
import com.geode.launcher.ui.theme.Typography
|
||||||
import com.geode.launcher.utils.Constants
|
import com.geode.launcher.utils.Constants
|
||||||
import com.geode.launcher.utils.GamePackageUtils
|
import com.geode.launcher.utils.GamePackageUtils
|
||||||
import com.geode.launcher.utils.GeodeUtils
|
import com.geode.launcher.utils.GeodeUtils
|
||||||
import com.geode.launcher.utils.PreferenceUtils
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
|
import com.geode.launcher.utils.useCountdownTimer
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
||||||
@@ -58,7 +71,7 @@ fun UpdateWarning(inSafeMode: Boolean = false, onDismiss: () -> Unit) {
|
|||||||
AlertDialog(
|
AlertDialog(
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_warning),
|
Icons.Filled.Warning,
|
||||||
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
|
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -103,11 +116,11 @@ fun SafeModeDialog(onDismiss: () -> Unit, onLaunch: () -> Unit) {
|
|||||||
AlertDialog(
|
AlertDialog(
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_shield_alert),
|
Icons.Filled.Warning,
|
||||||
contentDescription = null
|
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
title = { Text(stringResource(R.string.preference_launch_safe_mode_confirm_title)) },
|
title = { Text(stringResource(R.string.safe_mode_enable_title)) },
|
||||||
text = { Text(stringResource(R.string.safe_mode_enable_description)) },
|
text = { Text(stringResource(R.string.safe_mode_enable_description)) },
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = onDismiss) {
|
TextButton(onClick = onDismiss) {
|
||||||
@@ -184,12 +197,105 @@ 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
|
@Composable
|
||||||
fun UnsupportedVersionWarning(modifier: Modifier = Modifier) {
|
fun UnsupportedVersionWarning(modifier: Modifier = Modifier) {
|
||||||
InlineDialog(
|
InlineDialog(
|
||||||
headline = {
|
headline = {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_warning),
|
Icons.Default.Warning,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(28.dp)
|
modifier = Modifier.size(28.dp)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.displayCutoutPadding
|
import androidx.compose.foundation.layout.displayCutoutPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
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.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
@@ -26,6 +30,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -36,15 +41,14 @@ import com.geode.launcher.ui.theme.Theme
|
|||||||
import com.geode.launcher.updater.ReleaseManager
|
import com.geode.launcher.updater.ReleaseManager
|
||||||
import com.geode.launcher.utils.Constants
|
import com.geode.launcher.utils.Constants
|
||||||
import com.geode.launcher.utils.GamePackageUtils
|
import com.geode.launcher.utils.GamePackageUtils
|
||||||
import com.geode.launcher.utils.GeodeUtils
|
|
||||||
import com.geode.launcher.utils.PreferenceUtils
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
enum class LaunchNotificationType {
|
enum class LaunchNotificationType {
|
||||||
GEODE_UPDATED, LAUNCHER_UPDATE_AVAILABLE, UPDATE_FAILED, UNSUPPORTED_VERSION, FAILED_LOAD;
|
GEODE_UPDATED, LAUNCHER_UPDATE_AVAILABLE, UPDATE_FAILED, UNSUPPORTED_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun determineDisplayedCards(context: Context, geodeFailed: Boolean = false): List<LaunchNotificationType> {
|
fun determineDisplayedCards(context: Context): List<LaunchNotificationType> {
|
||||||
val cards = mutableListOf<LaunchNotificationType>()
|
val cards = mutableListOf<LaunchNotificationType>()
|
||||||
|
|
||||||
if (GamePackageUtils.getGameVersionCode(context.packageManager) < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
|
if (GamePackageUtils.getGameVersionCode(context.packageManager) < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
|
||||||
@@ -52,8 +56,10 @@ fun determineDisplayedCards(context: Context, geodeFailed: Boolean = false): Lis
|
|||||||
}
|
}
|
||||||
|
|
||||||
val releaseManager = ReleaseManager.get(context)
|
val releaseManager = ReleaseManager.get(context)
|
||||||
val availableUpdate = releaseManager.availableLauncherUpdateTag.value
|
val loadAutomatically = PreferenceUtils.get(context).getBoolean(PreferenceUtils.Key.LOAD_AUTOMATICALLY)
|
||||||
if (availableUpdate != null && !releaseManager.dismissedLauncherUpdate) {
|
|
||||||
|
val availableUpdate = releaseManager.availableLauncherUpdate.value
|
||||||
|
if (loadAutomatically && availableUpdate != null) {
|
||||||
cards.add(LaunchNotificationType.LAUNCHER_UPDATE_AVAILABLE)
|
cards.add(LaunchNotificationType.LAUNCHER_UPDATE_AVAILABLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,10 +73,6 @@ fun determineDisplayedCards(context: Context, geodeFailed: Boolean = false): Lis
|
|||||||
cards.add(LaunchNotificationType.UPDATE_FAILED)
|
cards.add(LaunchNotificationType.UPDATE_FAILED)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (geodeFailed) {
|
|
||||||
cards.add(LaunchNotificationType.FAILED_LOAD)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cards
|
return cards
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,30 +107,27 @@ fun NotificationCardFromType(type: LaunchNotificationType) {
|
|||||||
OutdatedVersionContent()
|
OutdatedVersionContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchNotificationType.FAILED_LOAD -> {
|
|
||||||
AnimatedNotificationCard {
|
|
||||||
ForwardCompatibilityContent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LaunchNotification(cards: List<LaunchNotificationType>) {
|
fun LaunchNotification() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
|
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
|
||||||
val theme = Theme.fromInt(themeOption)
|
val theme = Theme.fromInt(themeOption)
|
||||||
|
|
||||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||||
|
|
||||||
|
val cards = determineDisplayedCards(context)
|
||||||
|
|
||||||
CompositionLocalProvider(LocalTheme provides theme) {
|
CompositionLocalProvider(LocalTheme provides theme) {
|
||||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||||
// surface is not in use, so this is unfortunately not provided
|
// surface is not in use, so this is unfortunately not provided
|
||||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
||||||
Column(
|
Column(
|
||||||
modifier = if (GeodeUtils.handleSafeArea)
|
modifier = Modifier.displayCutoutPadding()
|
||||||
Modifier.displayCutoutPadding()
|
|
||||||
else Modifier
|
|
||||||
) {
|
) {
|
||||||
cards.forEach {
|
cards.forEach {
|
||||||
NotificationCardFromType(it)
|
NotificationCardFromType(it)
|
||||||
@@ -200,7 +199,7 @@ fun LauncherUpdateContent(modifier: Modifier = Modifier) {
|
|||||||
},
|
},
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_info),
|
Icons.Filled.Info,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -208,7 +207,6 @@ fun LauncherUpdateContent(modifier: Modifier = Modifier) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateNotificationContent(modifier: Modifier = Modifier) {
|
fun UpdateNotificationContent(modifier: Modifier = Modifier) {
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -225,7 +223,6 @@ fun UpdateNotificationContent(modifier: Modifier = Modifier) {
|
|||||||
modifier = modifier.width(IntrinsicSize.Max)
|
modifier = modifier.width(IntrinsicSize.Max)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OutdatedVersionContent(modifier: Modifier = Modifier) {
|
fun OutdatedVersionContent(modifier: Modifier = Modifier) {
|
||||||
@@ -235,7 +232,7 @@ fun OutdatedVersionContent(modifier: Modifier = Modifier) {
|
|||||||
},
|
},
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_warning),
|
Icons.Filled.Warning,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -254,29 +251,10 @@ fun UpdateFailedContent(modifier: Modifier = Modifier) {
|
|||||||
},
|
},
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_warning),
|
Icons.Filled.Warning,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = modifier.width(IntrinsicSize.Max)
|
modifier = modifier.width(IntrinsicSize.Max)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ForwardCompatibilityContent(modifier: Modifier = Modifier) {
|
|
||||||
ListItem(
|
|
||||||
headlineContent = {
|
|
||||||
Text(text = stringResource(id = R.string.launcher_notification_forwards_compatibility))
|
|
||||||
},
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_warning),
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
supportingContent = {
|
|
||||||
Text(text = stringResource(id = R.string.launcher_notification_forwards_compatibility_description))
|
|
||||||
},
|
|
||||||
modifier = modifier.width(IntrinsicSize.Max)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -18,11 +18,7 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.takeWhile
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
data class LaunchArguments(
|
private const val COOLDOWN_LENGTH_MS = 3000L
|
||||||
val autoSafeMode: Boolean,
|
|
||||||
val forcePause: Boolean,
|
|
||||||
val forceLaunch: Boolean
|
|
||||||
)
|
|
||||||
|
|
||||||
class LaunchViewModel(private val application: Application): ViewModel() {
|
class LaunchViewModel(private val application: Application): ViewModel() {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -41,13 +37,7 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
|||||||
private var readyTimer: CountDownTimer? = null
|
private var readyTimer: CountDownTimer? = null
|
||||||
|
|
||||||
private fun initReadyTimer() {
|
private fun initReadyTimer() {
|
||||||
val cooldownLength = PreferenceUtils.get(application).getInt(PreferenceUtils.Key.WAIT_PERIOD) * 1000L
|
readyTimer = object : CountDownTimer(COOLDOWN_LENGTH_MS, 1000) {
|
||||||
if (cooldownLength == 0L) {
|
|
||||||
readyTimerPassed = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
readyTimer = object : CountDownTimer(cooldownLength, 1000) {
|
|
||||||
override fun onTick(millisUntilFinished: Long) {
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
// no tick necessary
|
// no tick necessary
|
||||||
}
|
}
|
||||||
@@ -65,25 +55,17 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class LaunchCancelReason {
|
enum class LaunchCancelReason {
|
||||||
MANUAL, AUTOMATIC, LAST_LAUNCH_CRASHED, LAST_LAUNCH_CRASHED_AUTOMATIC,
|
MANUAL, AUTOMATIC, LAST_LAUNCH_CRASHED, GEODE_NOT_FOUND, GAME_MISSING, GAME_OUTDATED;
|
||||||
GEODE_NOT_FOUND, GAME_MISSING, GAME_OUTDATED;
|
|
||||||
|
|
||||||
fun allowsRetry() = when (this) {
|
fun allowsRetry() = when (this) {
|
||||||
MANUAL,
|
MANUAL,
|
||||||
AUTOMATIC,
|
AUTOMATIC,
|
||||||
LAST_LAUNCH_CRASHED,
|
LAST_LAUNCH_CRASHED,
|
||||||
LAST_LAUNCH_CRASHED_AUTOMATIC,
|
|
||||||
GEODE_NOT_FOUND -> true
|
GEODE_NOT_FOUND -> true
|
||||||
GAME_MISSING,
|
GAME_MISSING,
|
||||||
GAME_OUTDATED -> false
|
GAME_OUTDATED -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isCrash() = when (this) {
|
|
||||||
LAST_LAUNCH_CRASHED,
|
|
||||||
LAST_LAUNCH_CRASHED_AUTOMATIC -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isGameInstallIssue() = when (this) {
|
fun isGameInstallIssue() = when (this) {
|
||||||
GAME_OUTDATED,
|
GAME_OUTDATED,
|
||||||
GAME_MISSING -> true
|
GAME_MISSING -> true
|
||||||
@@ -98,30 +80,18 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
|||||||
data class Cancelled(val reason: LaunchCancelReason, val inProgress: Boolean = false) : LaunchUIState()
|
data class Cancelled(val reason: LaunchCancelReason, val inProgress: Boolean = false) : LaunchUIState()
|
||||||
data object Working : LaunchUIState()
|
data object Working : LaunchUIState()
|
||||||
data object Ready : 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>(LaunchUIState.Initial)
|
private val _uiState = MutableStateFlow<LaunchUIState>(LaunchUIState.Initial)
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
val nextLauncherUpdate = ReleaseManager.get(application).availableLauncherUpdateTag
|
val nextLauncherUpdate = ReleaseManager.get(application).availableLauncherUpdate
|
||||||
|
|
||||||
var loadFailure: LoadFailureInfo? = null
|
var loadFailure: LoadFailureInfo? = null
|
||||||
var launchArguments: LaunchArguments? = null
|
|
||||||
|
|
||||||
private var hasCheckedForUpdates = false
|
private var hasCheckedForUpdates = false
|
||||||
private var isCancelling = false
|
private var isCancelling = false
|
||||||
private var hasManuallyStarted = false
|
private var hasManuallyStarted = false
|
||||||
|
|
||||||
private var restoreUpdateState: LaunchUIState? = null
|
|
||||||
|
|
||||||
private suspend fun determineUpdateStatus() {
|
private suspend fun determineUpdateStatus() {
|
||||||
ReleaseManager.get(application).uiState.takeWhile {
|
ReleaseManager.get(application).uiState.takeWhile {
|
||||||
it is ReleaseManager.ReleaseManagerState.InUpdateCheck || it is ReleaseManager.ReleaseManagerState.InDownload
|
it is ReleaseManager.ReleaseManagerState.InUpdateCheck || it is ReleaseManager.ReleaseManagerState.InDownload
|
||||||
@@ -136,19 +106,12 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
|||||||
|
|
||||||
// flow terminates once update check is finished
|
// flow terminates once update check is finished
|
||||||
hasCheckedForUpdates = true
|
hasCheckedForUpdates = true
|
||||||
|
|
||||||
val currentState = restoreUpdateState
|
|
||||||
if (currentState != null) {
|
|
||||||
_uiState.emit(currentState)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
preReadyCheck()
|
preReadyCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun currentCrashInfo(): LoadFailureInfo? {
|
fun currentCrashInfo(): LoadFailureInfo? {
|
||||||
val currentState = _uiState.value
|
val currentState = _uiState.value
|
||||||
if (currentState is LaunchUIState.Cancelled && currentState.reason.isCrash()) {
|
if (currentState is LaunchUIState.Cancelled && currentState.reason == LaunchCancelReason.LAST_LAUNCH_CRASHED) {
|
||||||
return loadFailure
|
return loadFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,12 +120,12 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
|||||||
|
|
||||||
fun clearCrashInfo() {
|
fun clearCrashInfo() {
|
||||||
val currentState = _uiState.value
|
val currentState = _uiState.value
|
||||||
if (currentState is LaunchUIState.Cancelled && currentState.reason.isCrash()) {
|
if (currentState is LaunchUIState.Cancelled && currentState.reason == LaunchCancelReason.LAST_LAUNCH_CRASHED) {
|
||||||
loadFailure = null
|
loadFailure = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun preReadyCheck() {
|
private suspend fun preReadyCheck() {
|
||||||
if (isCancelling) {
|
if (isCancelling) {
|
||||||
// don't let the game start if cancelled
|
// don't let the game start if cancelled
|
||||||
return
|
return
|
||||||
@@ -171,8 +134,7 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
|||||||
val packageManager = application.packageManager
|
val packageManager = application.packageManager
|
||||||
|
|
||||||
if (!GamePackageUtils.isGameInstalled(packageManager)) {
|
if (!GamePackageUtils.isGameInstalled(packageManager)) {
|
||||||
_uiState.tryEmit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_MISSING))
|
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_MISSING))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ReleaseManager.get(application).isInUpdate) {
|
if (ReleaseManager.get(application).isInUpdate) {
|
||||||
@@ -182,48 +144,32 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// last launch crashed, so cancel
|
|
||||||
if (loadFailure != null) {
|
if (loadFailure != null) {
|
||||||
// show a different message if this is the first thing users see
|
// last launch crashed, so cancel
|
||||||
val cancelType = if (_uiState.value == LaunchUIState.Initial) {
|
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.LAST_LAUNCH_CRASHED))
|
||||||
LaunchCancelReason.LAST_LAUNCH_CRASHED_AUTOMATIC
|
|
||||||
} else LaunchCancelReason.LAST_LAUNCH_CRASHED
|
|
||||||
|
|
||||||
_uiState.tryEmit(LaunchUIState.Cancelled(cancelType))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GamePackageUtils.getGameVersionCode(packageManager) < Constants.SUPPORTED_VERSION_CODE_MIN) {
|
if (GamePackageUtils.getGameVersionCode(packageManager) < Constants.SUPPORTED_VERSION_CODE_MIN) {
|
||||||
_uiState.tryEmit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_OUTDATED))
|
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_OUTDATED))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!LaunchUtils.isGeodeInstalled(application)) {
|
if (!LaunchUtils.isGeodeInstalled(application)) {
|
||||||
_uiState.tryEmit(LaunchUIState.Cancelled(LaunchCancelReason.GEODE_NOT_FOUND))
|
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GEODE_NOT_FOUND))
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
val loadAutomatically = PreferenceUtils.get(application).getBoolean(PreferenceUtils.Key.LOAD_AUTOMATICALLY)
|
||||||
val forcePause = launchArguments?.forcePause == true
|
if (!loadAutomatically && !hasManuallyStarted) {
|
||||||
if (!hasManuallyStarted && (forcePause || !loadAutomatically)) {
|
|
||||||
return cancelLaunch(true)
|
return cancelLaunch(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!readyTimerPassed && !hasManuallyStarted) {
|
if (!readyTimerPassed && !hasManuallyStarted) {
|
||||||
_uiState.tryEmit(LaunchUIState.Working)
|
return _uiState.emit(LaunchUIState.Working)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_uiState.tryEmit(LaunchUIState.Ready)
|
_uiState.emit(LaunchUIState.Ready)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun beginLaunchFlow(isRestart: Boolean = false) {
|
suspend fun beginLaunchFlow(isRestart: Boolean = false) {
|
||||||
if (isRestart) {
|
if (isRestart) {
|
||||||
hasManuallyStarted = true
|
hasManuallyStarted = true
|
||||||
}
|
}
|
||||||
@@ -239,44 +185,23 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
|||||||
isCancelling = false
|
isCancelling = false
|
||||||
|
|
||||||
if (!GamePackageUtils.isGameInstalled(application.packageManager)) {
|
if (!GamePackageUtils.isGameInstalled(application.packageManager)) {
|
||||||
_uiState.tryEmit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_MISSING))
|
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GAME_MISSING))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (readyTimer == null) {
|
if (readyTimer == null) {
|
||||||
initReadyTimer()
|
initReadyTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
val releaseManager = ReleaseManager.get(application)
|
|
||||||
|
|
||||||
val hasGeode = LaunchUtils.isGeodeInstalled(application)
|
val hasGeode = LaunchUtils.isGeodeInstalled(application)
|
||||||
val shouldUpdate = PreferenceUtils.get(application).getBoolean(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
|
val shouldUpdate = PreferenceUtils.get(application).getBoolean(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
|
||||||
|
|
||||||
if ((shouldUpdate && !hasCheckedForUpdates) || !hasGeode) {
|
if ((shouldUpdate && !hasCheckedForUpdates) || !hasGeode) {
|
||||||
releaseManager.checkForUpdates(false)
|
ReleaseManager.get(application).checkForUpdates(false)
|
||||||
} else {
|
|
||||||
releaseManager.afterUseCachedUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRestart) {
|
|
||||||
releaseManager.dismissedLauncherUpdate = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preReadyCheck()
|
preReadyCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retryUpdate() {
|
suspend fun cancelLaunch(isAutomatic: Boolean = false) {
|
||||||
ReleaseManager.get(application).checkForUpdates(true)
|
|
||||||
if (ReleaseManager.get(application).isInUpdate) {
|
|
||||||
restoreUpdateState = _uiState.value
|
|
||||||
viewModelScope.launch {
|
|
||||||
determineUpdateStatus()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelLaunch(isAutomatic: Boolean = false) {
|
|
||||||
// no need to double cancel
|
// no need to double cancel
|
||||||
if (_uiState.value is LaunchUIState.Cancelled) {
|
if (_uiState.value is LaunchUIState.Cancelled) {
|
||||||
return
|
return
|
||||||
@@ -285,17 +210,14 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
|||||||
val reason = if (isAutomatic) LaunchCancelReason.AUTOMATIC else LaunchCancelReason.MANUAL
|
val reason = if (isAutomatic) LaunchCancelReason.AUTOMATIC else LaunchCancelReason.MANUAL
|
||||||
|
|
||||||
isCancelling = true
|
isCancelling = true
|
||||||
|
_uiState.emit(LaunchUIState.Cancelled(reason, true))
|
||||||
|
|
||||||
val releaseManager = ReleaseManager.get(application)
|
val releaseManager = ReleaseManager.get(application)
|
||||||
if (releaseManager.isInUpdate) {
|
if (releaseManager.isInUpdate) {
|
||||||
_uiState.tryEmit(LaunchUIState.Cancelled(reason, true))
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
releaseManager.cancelUpdate()
|
releaseManager.cancelUpdate()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
_uiState.tryEmit(LaunchUIState.Cancelled(reason, false))
|
_uiState.emit(LaunchUIState.Cancelled(reason, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
package com.geode.launcher.main
|
package com.geode.launcher.main
|
||||||
|
|
||||||
import android.app.PendingIntent
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageInstaller
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.provider.DocumentsContract
|
||||||
|
import android.text.format.Formatter
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@@ -28,17 +29,16 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -47,30 +47,26 @@ import androidx.compose.ui.platform.LocalUriHandler
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextLinkStyles
|
import androidx.compose.ui.text.TextLinkStyles
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.geode.launcher.InstallReceiver
|
|
||||||
import com.geode.launcher.R
|
import com.geode.launcher.R
|
||||||
import com.geode.launcher.main.LauncherUpdater.downloadUpdate
|
import com.geode.launcher.UserDirectoryProvider
|
||||||
import com.geode.launcher.ui.theme.Typography
|
import com.geode.launcher.ui.theme.Typography
|
||||||
import com.geode.launcher.updater.ReleaseManager
|
import com.geode.launcher.updater.ReleaseManager
|
||||||
|
import com.geode.launcher.updater.ReleaseViewModel
|
||||||
import com.geode.launcher.utils.LaunchUtils
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
import com.geode.launcher.utils.PreferenceUtils
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
import com.mikepenz.markdown.compose.LocalBulletListHandler
|
import com.mikepenz.markdown.compose.LocalBulletListHandler
|
||||||
import com.mikepenz.markdown.m3.Markdown
|
import com.mikepenz.markdown.m3.Markdown
|
||||||
import com.mikepenz.markdown.m3.markdownTypography
|
import com.mikepenz.markdown.m3.markdownTypography
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.runInterruptible
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
|
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.net.ConnectException
|
||||||
|
import java.net.UnknownHostException
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
import kotlin.time.toJavaInstant
|
import kotlin.time.toJavaInstant
|
||||||
|
|
||||||
fun clearDownloadedApks(context: Context) {
|
fun clearDownloadedApks(context: Context) {
|
||||||
@@ -90,220 +86,74 @@ fun clearDownloadedApks(context: Context) {
|
|||||||
it.delete()
|
it.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
context.filesDir.listFiles { _, name -> name.lowercase().endsWith(".apk") }?.forEach {
|
|
||||||
it.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
preferenceUtils.setBoolean(PreferenceUtils.Key.CLEANUP_APKS, false)
|
preferenceUtils.setBoolean(PreferenceUtils.Key.CLEANUP_APKS, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// lazy singleton idc
|
fun generateInstallIntent(uri: Uri): Intent {
|
||||||
object LauncherUpdater {
|
// maybe one day i'll rewrite this to use packageinstaller. not today
|
||||||
enum class LauncherUpdateState {
|
return Intent(Intent.ACTION_INSTALL_PACKAGE).apply {
|
||||||
Began,
|
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
Downloading,
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
Active,
|
|
||||||
Inactive,
|
|
||||||
Finished;
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow<LauncherUpdateState>(LauncherUpdateState.Inactive)
|
|
||||||
val uiState = _uiState.asStateFlow()
|
|
||||||
|
|
||||||
suspend fun resetState() {
|
|
||||||
_uiState.emit(LauncherUpdateState.Inactive)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun downloadUpdate(context: Context): Result<File> {
|
|
||||||
_uiState.emit(LauncherUpdateState.Downloading)
|
|
||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
ReleaseManager.get(context)
|
|
||||||
.downloadLatestLauncherUpdate()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun installPackage(context: Context, file: File) {
|
fun installLauncherUpdate(context: Context) {
|
||||||
_uiState.emit(LauncherUpdateState.Began)
|
val nextUpdate = ReleaseManager.get(context).availableLauncherUpdate.value
|
||||||
|
val launcherDownload = nextUpdate?.getDownload()
|
||||||
|
|
||||||
val installer = context.packageManager.packageInstaller
|
if (launcherDownload != null) {
|
||||||
|
val outputFile = launcherDownload.filename
|
||||||
|
val baseDirectory = LaunchUtils.getBaseDirectory(context)
|
||||||
|
|
||||||
installer.registerSessionCallback(object : PackageInstaller.SessionCallback() {
|
val outputPathFile = File(baseDirectory, outputFile)
|
||||||
override fun onActiveChanged(sessionId: Int, active: Boolean) {
|
if (!outputPathFile.exists()) {
|
||||||
if (active) {
|
|
||||||
_uiState.tryEmit(LauncherUpdateState.Active)
|
|
||||||
} else {
|
|
||||||
_uiState.tryEmit(LauncherUpdateState.Began)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) { runInterruptible {
|
|
||||||
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)
|
|
||||||
installer.openSession(sessionId).use { session ->
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun installLauncherUpdate(context: Context) {
|
|
||||||
val launcherDownload = downloadUpdate(context)
|
|
||||||
.getOrElse { e ->
|
|
||||||
e.printStackTrace()
|
|
||||||
Toast.makeText(
|
|
||||||
context, context.getString(
|
|
||||||
R.string.launcher_update_download_failed,
|
|
||||||
e.cause
|
|
||||||
), Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!launcherDownload.exists()) {
|
val outputPath = "${UserDirectoryProvider.ROOT}${outputFile}"
|
||||||
return
|
|
||||||
}
|
val uri = DocumentsContract.buildDocumentUri("${context.packageName}.user", outputPath)
|
||||||
|
|
||||||
PreferenceUtils.get(context).setBoolean(PreferenceUtils.Key.CLEANUP_APKS, true)
|
PreferenceUtils.get(context).setBoolean(PreferenceUtils.Key.CLEANUP_APKS, true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
LauncherUpdater.installPackage(context, launcherDownload)
|
val intent = generateInstallIntent(uri)
|
||||||
} catch (e: Exception) {
|
context.startActivity(intent)
|
||||||
e.printStackTrace()
|
} catch (_: ActivityNotFoundException) {
|
||||||
val msg = context.getString(R.string.launcher_self_update_failed, e.message)
|
// if it fails to install, just open it in the browser
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
try {
|
||||||
|
val downloadUrl = launcherDownload.url
|
||||||
LauncherUpdater.resetState()
|
val downloadIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
}
|
data = Uri.parse(downloadUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
context.startActivity(downloadIntent)
|
||||||
fun LauncherUpdateDialog(isCancelling: Boolean, onDismiss: () -> Unit) {
|
} catch (_: ActivityNotFoundException) {
|
||||||
val updateState by LauncherUpdater.uiState.collectAsState()
|
Toast.makeText(context, context.getString(R.string.no_activity_found), Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
LaunchedEffect(updateState) {
|
|
||||||
if (updateState == LauncherUpdater.LauncherUpdateState.Finished) {
|
|
||||||
onDismiss()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AlertDialog(
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_update),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
if (isCancelling) {
|
|
||||||
Text(stringResource(R.string.release_fetch_cancelling))
|
|
||||||
} else if (updateState == LauncherUpdater.LauncherUpdateState.Downloading) {
|
|
||||||
Text(stringResource(R.string.launcher_downloading_update))
|
|
||||||
} else {
|
} else {
|
||||||
Text(stringResource(R.string.launcher_self_update_progress))
|
Toast.makeText(context, context.getString(R.string.release_fetch_no_releases), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
},
|
|
||||||
text = {
|
|
||||||
LinearProgressIndicator()
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onDismiss, enabled = !isCancelling) {
|
|
||||||
Text(stringResource(R.string.message_box_cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDismissRequest = {}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val releaseManager = remember { ReleaseManager.get(context) }
|
val nextRelease = remember { ReleaseManager.get(context).availableLauncherUpdate.value }
|
||||||
|
|
||||||
val nextReleaseName by releaseManager.availableLauncherUpdateTag.collectAsState()
|
|
||||||
val nextRelease by releaseManager.availableLauncherUpdateDetails.collectAsState()
|
|
||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
var updateJob by remember { mutableStateOf<Job?>(null) }
|
if (nextRelease != null) {
|
||||||
var cancellingUpdate by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
var beginUpdate by remember { mutableStateOf(false) }
|
|
||||||
if (beginUpdate) {
|
|
||||||
LauncherUpdateDialog(cancellingUpdate, onDismiss = {
|
|
||||||
cancellingUpdate = true
|
|
||||||
|
|
||||||
coroutineScope.launch {
|
|
||||||
updateJob?.cancelAndJoin()
|
|
||||||
|
|
||||||
beginUpdate = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
if (nextReleaseName != null && nextRelease == null) {
|
|
||||||
coroutineScope.launch { withContext(Dispatchers.IO) {
|
|
||||||
ReleaseManager.get(context)
|
|
||||||
.checkLauncherUpdate()
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val nextReleaseNameSaved = nextReleaseName
|
|
||||||
if (nextReleaseNameSaved != null) {
|
|
||||||
ModalBottomSheet(onDismissRequest = { onDismiss() }, sheetState = sheetState) {
|
ModalBottomSheet(onDismissRequest = { onDismiss() }, sheetState = sheetState) {
|
||||||
Box(Modifier.verticalScroll(rememberScrollState())) {
|
Box(Modifier.verticalScroll(rememberScrollState())) {
|
||||||
Column(modifier = Modifier.padding(horizontal = 24.dp)) {
|
Column(modifier = Modifier.padding(horizontal = 24.dp)) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.launcher_update_name, nextReleaseNameSaved),
|
stringResource(R.string.launcher_update_name, nextRelease.release.tagName),
|
||||||
style = Typography.headlineSmall,
|
style = Typography.headlineSmall,
|
||||||
)
|
)
|
||||||
|
|
||||||
val nextRelease = nextRelease
|
|
||||||
if (nextRelease != null) {
|
|
||||||
val releasedTime = remember {
|
val releasedTime = remember {
|
||||||
DateFormat.getDateInstance().format(
|
DateFormat.getDateInstance().format(
|
||||||
Date.from(nextRelease.release.publishedAt.toJavaInstant())
|
Date.from(nextRelease.release.publishedAt.toJavaInstant())
|
||||||
@@ -317,14 +167,7 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
|||||||
|
|
||||||
Row(modifier = Modifier.padding(vertical = 12.dp)) {
|
Row(modifier = Modifier.padding(vertical = 12.dp)) {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = { installLauncherUpdate(context) },
|
||||||
updateJob = coroutineScope.launch {
|
|
||||||
cancellingUpdate = false
|
|
||||||
beginUpdate = true
|
|
||||||
|
|
||||||
installLauncherUpdate(context)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
Icon(painterResource(R.drawable.icon_download), contentDescription = null)
|
Icon(painterResource(R.drawable.icon_download), contentDescription = null)
|
||||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
@@ -366,16 +209,6 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 12.dp)
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.size(4.dp))
|
Spacer(modifier = Modifier.size(4.dp))
|
||||||
}
|
}
|
||||||
@@ -387,7 +220,6 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
|
fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
var showInfoDialog by remember { mutableStateOf(false) }
|
var showInfoDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -397,22 +229,6 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var updateJob by remember { mutableStateOf<Job?>(null) }
|
|
||||||
var cancellingUpdate by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
var beginUpdate by remember { mutableStateOf(false) }
|
|
||||||
if (beginUpdate) {
|
|
||||||
LauncherUpdateDialog(cancellingUpdate) {
|
|
||||||
cancellingUpdate = true
|
|
||||||
coroutineScope.launch {
|
|
||||||
updateJob?.cancelAndJoin()
|
|
||||||
updateJob = null
|
|
||||||
|
|
||||||
beginUpdate = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ElevatedCard(modifier) {
|
ElevatedCard(modifier) {
|
||||||
Column(
|
Column(
|
||||||
// buttons add enough padding already, lower to compensate
|
// buttons add enough padding already, lower to compensate
|
||||||
@@ -437,14 +253,7 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = { installLauncherUpdate(context) },
|
||||||
updateJob = coroutineScope.launch {
|
|
||||||
cancellingUpdate = false
|
|
||||||
beginUpdate = true
|
|
||||||
|
|
||||||
installLauncherUpdate(context)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.launcher_install))
|
Text(stringResource(R.string.launcher_install))
|
||||||
}
|
}
|
||||||
@@ -452,3 +261,174 @@ 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(
|
||||||
|
if (outOf != null) stringResource(
|
||||||
|
R.string.release_fetch_downloading,
|
||||||
|
downloaded,
|
||||||
|
outOf
|
||||||
|
) else stringResource(
|
||||||
|
R.string.release_fetch_downloading_indeterminate,
|
||||||
|
downloaded,
|
||||||
|
),
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,11 +19,10 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.selection.selectable
|
import androidx.compose.foundation.selection.selectable
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -32,7 +31,6 @@ import androidx.compose.material3.RadioButton
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -46,7 +44,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -87,40 +84,6 @@ class ApplicationIconActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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) {
|
fun onSelectIcon(context: Context, selectedIcon: ApplicationIconDetails) {
|
||||||
val packageManager = context.packageManager
|
val packageManager = context.packageManager
|
||||||
|
|
||||||
@@ -145,14 +108,7 @@ fun onSelectIcon(context: Context, selectedIcon: ApplicationIconDetails) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun IconCard(iconData: ApplicationIconDetails, selected: Boolean, modifier: Modifier = Modifier) {
|
fun IconCard(iconData: ApplicationIconDetails, selected: Boolean, modifier: Modifier = Modifier) {
|
||||||
var showSelection by remember { mutableStateOf(false) }
|
val context = LocalContext.current
|
||||||
|
|
||||||
if (showSelection) {
|
|
||||||
IconSelectConfirmDialog(iconData) {
|
|
||||||
showSelection = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -162,8 +118,7 @@ fun IconCard(iconData: ApplicationIconDetails, selected: Boolean, modifier: Modi
|
|||||||
.selectable(
|
.selectable(
|
||||||
selected = selected,
|
selected = selected,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!selected)
|
onSelectIcon(context, iconData)
|
||||||
showSelection = true
|
|
||||||
},
|
},
|
||||||
role = Role.RadioButton
|
role = Role.RadioButton
|
||||||
)
|
)
|
||||||
@@ -197,6 +152,8 @@ fun ApplicationIconScreen(
|
|||||||
) {
|
) {
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
|
|
||||||
|
var showMoreOptions by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -205,7 +162,7 @@ fun ApplicationIconScreen(
|
|||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_arrow_back),
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = stringResource(R.string.back_icon_alt)
|
contentDescription = stringResource(R.string.back_icon_alt)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -225,8 +182,7 @@ fun ApplicationIconScreen(
|
|||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth(),
|
||||||
.verticalScroll(rememberScrollState()),
|
|
||||||
verticalArrangement = Arrangement.SpaceEvenly,
|
verticalArrangement = Arrangement.SpaceEvenly,
|
||||||
) {
|
) {
|
||||||
val selectedIcon by PreferenceUtils.useStringPreference(PreferenceUtils.Key.SELECTED_ICON)
|
val selectedIcon by PreferenceUtils.useStringPreference(PreferenceUtils.Key.SELECTED_ICON)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.geode.launcher.preferences
|
package com.geode.launcher.preferences
|
||||||
|
|
||||||
import android.content.ClipData
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
@@ -18,6 +18,11 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
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.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -45,13 +50,13 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.ClipEntry
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalClipboard
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.platform.LocalResources
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
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.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
@@ -145,10 +150,7 @@ fun SelectLogLevelDialog(
|
|||||||
@Composable
|
@Composable
|
||||||
fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
|
fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
|
||||||
val haptics = LocalHapticFeedback.current
|
val haptics = LocalHapticFeedback.current
|
||||||
val clipboard = LocalClipboard.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
val resources = LocalResources.current
|
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -157,17 +159,7 @@ fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
|
|||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onLongPress = {
|
onLongPress = {
|
||||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
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
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -197,7 +189,6 @@ fun ApplicationLogsScreen(
|
|||||||
val isLoading by logViewModel.isLoading.collectAsState()
|
val isLoading by logViewModel.isLoading.collectAsState()
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val resources = LocalResources.current
|
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
@@ -245,7 +236,7 @@ fun ApplicationLogsScreen(
|
|||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_arrow_back),
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = stringResource(R.string.back_icon_alt)
|
contentDescription = stringResource(R.string.back_icon_alt)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -253,7 +244,7 @@ fun ApplicationLogsScreen(
|
|||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
saveLauncher.launch(resources.getString(R.string.application_logs_default_filename))
|
saveLauncher.launch(context.getString(R.string.application_logs_default_filename))
|
||||||
}
|
}
|
||||||
}, enabled = !logLines.isEmpty()) {
|
}, enabled = !logLines.isEmpty()) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -263,7 +254,7 @@ fun ApplicationLogsScreen(
|
|||||||
}
|
}
|
||||||
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
|
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_more_vert),
|
Icons.Filled.MoreVert,
|
||||||
contentDescription = stringResource(R.string.application_logs_more)
|
contentDescription = stringResource(R.string.application_logs_more)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -282,7 +273,7 @@ fun ApplicationLogsScreen(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (logViewModel.filterCrashes) {
|
if (logViewModel.filterCrashes) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_check),
|
Icons.Filled.Check,
|
||||||
contentDescription = stringResource(R.string.application_logs_crash_only_enabled_alt)
|
contentDescription = stringResource(R.string.application_logs_crash_only_enabled_alt)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material.icons.filled.Share
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -51,6 +56,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
@@ -163,7 +169,7 @@ fun CrashCard(crashDump: CrashDump, crashViewModel: CrashViewModel, modifier: Mo
|
|||||||
shareCrash(context, crashDump.filename)
|
shareCrash(context, crashDump.filename)
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_share),
|
Icons.Filled.Share,
|
||||||
contentDescription = stringResource(R.string.application_logs_share)
|
contentDescription = stringResource(R.string.application_logs_share)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -172,7 +178,7 @@ fun CrashCard(crashDump: CrashDump, crashViewModel: CrashViewModel, modifier: Mo
|
|||||||
crashViewModel.removeCrash(crashDump.filename)
|
crashViewModel.removeCrash(crashDump.filename)
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_delete),
|
Icons.Filled.Delete,
|
||||||
contentDescription = stringResource(R.string.application_crashes_delete_one)
|
contentDescription = stringResource(R.string.application_crashes_delete_one)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -201,7 +207,7 @@ fun CrashLogsScreen(
|
|||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_arrow_back),
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = stringResource(R.string.back_icon_alt)
|
contentDescription = stringResource(R.string.back_icon_alt)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -209,7 +215,7 @@ fun CrashLogsScreen(
|
|||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
|
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_more_vert),
|
Icons.Filled.MoreVert,
|
||||||
contentDescription = stringResource(R.string.application_logs_more)
|
contentDescription = stringResource(R.string.application_logs_more)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -232,42 +238,6 @@ fun CrashLogsScreen(
|
|||||||
showMoreOptions = false
|
showMoreOptions = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val developerMode by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
|
|
||||||
if (developerMode) {
|
|
||||||
|
|
||||||
if (crashViewModel.hasIndicator) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_remove_selection),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(stringResource(R.string.application_crashes_clear_indicator))
|
|
||||||
}, onClick = {
|
|
||||||
crashViewModel.clearIndicator()
|
|
||||||
showMoreOptions = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
DropdownMenuItem(
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_skull),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(stringResource(R.string.application_crashes_create_indicator))
|
|
||||||
}, onClick = {
|
|
||||||
crashViewModel.createIndicator()
|
|
||||||
showMoreOptions = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
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.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -36,7 +41,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -110,7 +114,7 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
|||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_arrow_back),
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = stringResource(R.string.back_icon_alt)
|
contentDescription = stringResource(R.string.back_icon_alt)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -157,14 +161,13 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
|||||||
}}
|
}}
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
SettingsCard(
|
SettingsCard(
|
||||||
title = stringResource(R.string.preference_force_hrr),
|
title = stringResource(R.string.preference_force_hrr),
|
||||||
preferenceKey = PreferenceUtils.Key.FORCE_HRR,
|
preferenceKey = PreferenceUtils.Key.FORCE_HRR,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
SettingsCard(
|
SettingsCard(
|
||||||
title = stringResource(R.string.preference_override_exceptions_name),
|
title = stringResource(R.string.preference_override_exceptions_name),
|
||||||
description = stringResource(R.string.preference_override_exceptions_description),
|
description = stringResource(R.string.preference_override_exceptions_description),
|
||||||
@@ -194,8 +197,6 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OptionsGroup(title = stringResource(R.string.preference_category_updater)) {
|
OptionsGroup(title = stringResource(R.string.preference_category_updater)) {
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
SettingsSelectCard(
|
SettingsSelectCard(
|
||||||
title = stringResource(R.string.preference_release_channel_tag_name),
|
title = stringResource(R.string.preference_release_channel_tag_name),
|
||||||
dialogTitle = stringResource(R.string.preference_release_channel_select),
|
dialogTitle = stringResource(R.string.preference_release_channel_select),
|
||||||
@@ -204,38 +205,15 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
|||||||
0 to stringResource(R.string.preference_release_channel_stable),
|
0 to stringResource(R.string.preference_release_channel_stable),
|
||||||
1 to stringResource(R.string.preference_release_channel_beta),
|
1 to stringResource(R.string.preference_release_channel_beta),
|
||||||
2 to stringResource(R.string.preference_release_channel_nightly),
|
2 to stringResource(R.string.preference_release_channel_nightly),
|
||||||
),
|
|
||||||
extraSelectBehavior = {
|
|
||||||
PreferenceUtils.get(context)
|
|
||||||
.setLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME, 0L)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
SettingsCard(
|
|
||||||
title = stringResource(R.string.preference_disable_update_cache),
|
|
||||||
preferenceKey = PreferenceUtils.Key.DISABLE_UPDATE_CACHE
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionsGroup(title = stringResource(R.string.preference_category_testing)) {
|
OptionsGroup(title = stringResource(R.string.preference_category_testing)) {
|
||||||
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(
|
SettingsCard(
|
||||||
title = stringResource(R.string.preference_disable_redesign),
|
title = stringResource(R.string.preference_enable_redesign),
|
||||||
preferenceKey = PreferenceUtils.Key.DISABLE_REDESIGN,
|
preferenceKey = PreferenceUtils.Key.ENABLE_REDESIGN,
|
||||||
)
|
)
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionsGroup(title = stringResource(R.string.preference_profiles)) {
|
OptionsGroup(title = stringResource(R.string.preference_profiles)) {
|
||||||
|
|||||||
@@ -1,482 +0,0 @@
|
|||||||
package com.geode.launcher.preferences
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import android.util.Xml
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.geode.launcher.R
|
|
||||||
import com.geode.launcher.utils.LaunchUtils
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.runInterruptible
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.coroutines.executeAsync
|
|
||||||
import okio.Buffer
|
|
||||||
import okio.ByteString.Companion.decodeBase64
|
|
||||||
import okio.GzipSource
|
|
||||||
import okio.buffer
|
|
||||||
import okio.source
|
|
||||||
import org.xmlpull.v1.XmlPullParser
|
|
||||||
import java.io.File
|
|
||||||
import kotlin.experimental.xor
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
const val TAG = "com.geode.launcher.SaveBackup"
|
|
||||||
|
|
||||||
data class UserDetails(
|
|
||||||
val userName: String,
|
|
||||||
val gjp2: String,
|
|
||||||
val accountId: Int,
|
|
||||||
val uuid: Int,
|
|
||||||
val udid: String,
|
|
||||||
val binaryVersion: Int,
|
|
||||||
)
|
|
||||||
|
|
||||||
enum class BackupParseFailure {
|
|
||||||
SAVE_MISSING,
|
|
||||||
DECODE_FAILED,
|
|
||||||
DETAILS_MISSING,
|
|
||||||
SAVE_TOO_LARGE
|
|
||||||
}
|
|
||||||
|
|
||||||
class BackupParseException(val failureCode: BackupParseFailure) : Exception("parse error: $failureCode")
|
|
||||||
|
|
||||||
enum class BackupUploadFailure {
|
|
||||||
GENERIC_ERROR,
|
|
||||||
NETWORK_ERROR,
|
|
||||||
INVALID_LOGIN
|
|
||||||
}
|
|
||||||
|
|
||||||
class BackupUploadException(val failureCode: BackupUploadFailure, val gjCode: Int = -1) : Exception("upload error: $failureCode")
|
|
||||||
|
|
||||||
suspend fun getAccountBackupUrl(backupClient: OkHttpClient, details: UserDetails): Result<HttpUrl> {
|
|
||||||
return try {
|
|
||||||
// accountID=%i&type=1&secret=Wmfd2893gb7
|
|
||||||
val body = FormBody.Builder()
|
|
||||||
.add("secret", "Wmfd2893gb7")
|
|
||||||
.add("type", "1")
|
|
||||||
.add("accountID", details.accountId.toString())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url("https://www.boomlings.com/database/getAccountURL.php")
|
|
||||||
.addHeader("User-Agent", "")
|
|
||||||
.post(body)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val call = backupClient.newCall(request)
|
|
||||||
call.executeAsync()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "getAccountBackupUrl failed: ${e.stackTraceToString()}")
|
|
||||||
return Result.failure(BackupUploadException(BackupUploadFailure.NETWORK_ERROR))
|
|
||||||
}.use { response ->
|
|
||||||
if (response.code != 200) {
|
|
||||||
return Result.failure(BackupUploadException(BackupUploadFailure.GENERIC_ERROR))
|
|
||||||
}
|
|
||||||
|
|
||||||
val url = response.body.string().toHttpUrlOrNull()
|
|
||||||
?: return Result.failure(BackupUploadException(BackupUploadFailure.GENERIC_ERROR))
|
|
||||||
|
|
||||||
Result.success(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decryptFileToBuffer(file: File, buffer: Buffer) = file.source().buffer()
|
|
||||||
.use { baseBuffer ->
|
|
||||||
val key = 0xb.toByte()
|
|
||||||
val nullByte = 0x0.toByte()
|
|
||||||
|
|
||||||
while (!baseBuffer.exhausted()) {
|
|
||||||
val b = baseBuffer.readByte()
|
|
||||||
val d = b.xor(key)
|
|
||||||
|
|
||||||
// gd sometimes encodes garbage after null byte, treat it like a c string
|
|
||||||
if (d == nullByte)
|
|
||||||
break
|
|
||||||
|
|
||||||
buffer.writeByte(d.toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateSaveString(gameManager: File, localLevels: File): String = Buffer()
|
|
||||||
.use { saveBuffer ->
|
|
||||||
decryptFileToBuffer(gameManager, saveBuffer)
|
|
||||||
saveBuffer.writeByte(';'.code)
|
|
||||||
decryptFileToBuffer(localLevels, saveBuffer)
|
|
||||||
|
|
||||||
saveBuffer.readUtf8()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateBackupRequest(details: UserDetails, url: HttpUrl, gameManager: File, localLevels: File): Request {
|
|
||||||
val saveData = generateSaveString(gameManager, localLevels)
|
|
||||||
|
|
||||||
// %s/database/accounts/backupGJAccountNew.php
|
|
||||||
val backupUrl = url.newBuilder()
|
|
||||||
.addPathSegments("database/accounts/backupGJAccountNew.php")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
// gameVersion=22&binaryVersion=%i&dvs=2&udid=%s&uuid=%i&accountID=%i&gjp2=%s&secret=Wmfv3899gc9&saveData=%s
|
|
||||||
val body = FormBody.Builder()
|
|
||||||
.add("gameVersion", "22")
|
|
||||||
.add("binaryVersion", details.binaryVersion.toString())
|
|
||||||
.add("dvs", "2")
|
|
||||||
.add("udid", details.udid)
|
|
||||||
.add("uuid", details.uuid.toString())
|
|
||||||
.add("accountID", details.accountId.toString())
|
|
||||||
.add("secret", "Wmfv3899gc9")
|
|
||||||
.add("gjp2", details.gjp2)
|
|
||||||
.add("saveData", saveData)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return Request.Builder()
|
|
||||||
.url(backupUrl)
|
|
||||||
.addHeader("User-Agent", "")
|
|
||||||
.post(body)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun performBackupRequest(backupClient: OkHttpClient, details: UserDetails, url: HttpUrl, gameManager: File, localLevels: File): Result<Unit> {
|
|
||||||
return try {
|
|
||||||
// we split this into multiple functions to free up as many references as we can
|
|
||||||
// go little gc go
|
|
||||||
backupClient.newCall(
|
|
||||||
generateBackupRequest(details, url, gameManager, localLevels)
|
|
||||||
).executeAsync()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "performBackupRequest failed: ${e.stackTraceToString()}")
|
|
||||||
return Result.failure(BackupUploadException(BackupUploadFailure.GENERIC_ERROR))
|
|
||||||
}.use { response ->
|
|
||||||
if (response.code != 200) {
|
|
||||||
return Result.failure(BackupUploadException(BackupUploadFailure.GENERIC_ERROR))
|
|
||||||
}
|
|
||||||
|
|
||||||
val responseBody = response.body.string().toIntOrNull()
|
|
||||||
?: return Result.failure(BackupUploadException(BackupUploadFailure.GENERIC_ERROR))
|
|
||||||
|
|
||||||
when (responseBody) {
|
|
||||||
1 -> Result.success(Unit)
|
|
||||||
-5 -> Result.failure(BackupUploadException(BackupUploadFailure.INVALID_LOGIN, responseBody))
|
|
||||||
else -> Result.failure(BackupUploadException(BackupUploadFailure.GENERIC_ERROR, responseBody))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun skip(parser: XmlPullParser) {
|
|
||||||
if (parser.eventType != XmlPullParser.START_TAG) {
|
|
||||||
throw IllegalStateException()
|
|
||||||
}
|
|
||||||
var depth = 1
|
|
||||||
while (depth != 0) {
|
|
||||||
when (parser.next()) {
|
|
||||||
XmlPullParser.END_TAG -> depth--
|
|
||||||
XmlPullParser.START_TAG -> depth++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getNextValue(parser: XmlPullParser): String? {
|
|
||||||
while (parser.eventType != XmlPullParser.START_TAG && parser.eventType != XmlPullParser.END_DOCUMENT) {
|
|
||||||
parser.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parser.eventType == XmlPullParser.START_TAG) {
|
|
||||||
// we could support t/f here but we also don't have to
|
|
||||||
if (parser.name != "s" && parser.name != "i") {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return parser.nextText()
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseSaveFile(parser: XmlPullParser): Result<UserDetails> = try {
|
|
||||||
var userName: String? = null
|
|
||||||
var gjp2: String? = null
|
|
||||||
var accountId: Int? = null
|
|
||||||
var uuid: Int? = null
|
|
||||||
var udid: String? = null
|
|
||||||
var binaryVersion: Int? = null
|
|
||||||
|
|
||||||
var inPlist = false
|
|
||||||
var inDict = false
|
|
||||||
|
|
||||||
var eventType = parser.eventType
|
|
||||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
|
||||||
when (eventType) {
|
|
||||||
XmlPullParser.START_TAG -> {
|
|
||||||
if (inDict) {
|
|
||||||
// ignore inner elements, just in case they conflict
|
|
||||||
if (parser.name != "k") {
|
|
||||||
skip(parser)
|
|
||||||
} else {
|
|
||||||
val keyName = parser.nextText()
|
|
||||||
when (keyName) {
|
|
||||||
"GJA_001" -> userName = getNextValue(parser)
|
|
||||||
"GJA_003" -> accountId = getNextValue(parser)?.toIntOrNull()
|
|
||||||
"GJA_005" -> gjp2 = getNextValue(parser)
|
|
||||||
"playerUserID" -> uuid = getNextValue(parser)?.toIntOrNull()
|
|
||||||
"playerUDID" -> udid = getNextValue(parser)
|
|
||||||
"binaryVersion" -> binaryVersion = getNextValue(parser)?.toIntOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// traverse to initial plist -> dict element
|
|
||||||
if (!inPlist && parser.name == "plist") {
|
|
||||||
inPlist = true
|
|
||||||
}
|
|
||||||
if (!inDict && parser.name == "dict") {
|
|
||||||
inDict = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eventType = parser.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userName == null || gjp2 == null || accountId == null || uuid == null || udid == null || binaryVersion == null) {
|
|
||||||
Result.failure(BackupParseException(BackupParseFailure.DETAILS_MISSING))
|
|
||||||
} else {
|
|
||||||
Result.success(
|
|
||||||
UserDetails(
|
|
||||||
userName = userName,
|
|
||||||
gjp2 = gjp2,
|
|
||||||
accountId = accountId,
|
|
||||||
uuid = uuid,
|
|
||||||
udid = udid,
|
|
||||||
binaryVersion = binaryVersion
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "parseSaveFile failed: ${e.stackTraceToString()}")
|
|
||||||
Result.failure(BackupParseException(BackupParseFailure.DECODE_FAILED))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decryptFileToString(file: File) = Buffer()
|
|
||||||
.use { decryptedBuffer ->
|
|
||||||
decryptFileToBuffer(file, decryptedBuffer)
|
|
||||||
decryptedBuffer.readUtf8()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getUserDetails(gameManager: File): Result<UserDetails> {
|
|
||||||
return try {
|
|
||||||
val base64Decoded = run {
|
|
||||||
// free decryptString after decoding
|
|
||||||
val decryptString = decryptFileToString(gameManager)
|
|
||||||
decryptString.decodeBase64()
|
|
||||||
} ?: return Result.failure(BackupParseException(BackupParseFailure.DECODE_FAILED))
|
|
||||||
|
|
||||||
// gzipSource will close the underlying buffer when closed
|
|
||||||
GzipSource(
|
|
||||||
Buffer().write(base64Decoded)
|
|
||||||
).buffer().use { gzipSource ->
|
|
||||||
runInterruptible {
|
|
||||||
val parser = Xml.newPullParser()
|
|
||||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
|
||||||
parser.setInput(gzipSource.inputStream(), null)
|
|
||||||
|
|
||||||
parseSaveFile(parser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "getUserDetails failed: ${e.stackTraceToString()}")
|
|
||||||
return Result.failure(BackupParseException(BackupParseFailure.DECODE_FAILED))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const val MB_TO_BYTES = 1048576 // 2^20
|
|
||||||
|
|
||||||
suspend fun backupSaveToCloud(context: Context): Result<Unit> {
|
|
||||||
val baseDirectory = LaunchUtils.getBaseDirectory(context)
|
|
||||||
|
|
||||||
val gameManager = File(baseDirectory,"/save/CCGameManager.dat")
|
|
||||||
val localLevels = File(baseDirectory, "/save/CCLocalLevels.dat")
|
|
||||||
|
|
||||||
if (!gameManager.isFile || !localLevels.isFile) {
|
|
||||||
return Result.failure(BackupParseException(BackupParseFailure.SAVE_MISSING))
|
|
||||||
}
|
|
||||||
|
|
||||||
// hardcode 32mb max size
|
|
||||||
// this is how the game checks it, which probably isn't the best
|
|
||||||
val totalSize = (gameManager.length() + localLevels.length()) / MB_TO_BYTES.toDouble()
|
|
||||||
println("Backup - calculated size of %.2fmb".format(totalSize))
|
|
||||||
|
|
||||||
if (totalSize > 32.0) {
|
|
||||||
return Result.failure(BackupParseException(BackupParseFailure.SAVE_TOO_LARGE))
|
|
||||||
}
|
|
||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
val userDetails = getUserDetails(gameManager).getOrElse {
|
|
||||||
return@withContext Result.failure(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
println("Backup - performing as ${userDetails.userName}, accountID=${userDetails.accountId}")
|
|
||||||
|
|
||||||
val backupClient = OkHttpClient.Builder()
|
|
||||||
.writeTimeout(500.seconds)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val backupUrl = getAccountBackupUrl(backupClient, userDetails).getOrElse {
|
|
||||||
return@withContext Result.failure(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
performBackupRequest(backupClient, userDetails, backupUrl, gameManager, localLevels).getOrElse {
|
|
||||||
return@withContext Result.failure(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
println("Backup completed!")
|
|
||||||
|
|
||||||
Result.success(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BackupConfirmDialog(onDismiss: () -> Unit, onEnable: () -> Unit) {
|
|
||||||
AlertDialog(
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_cloud_alert),
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
title = { Text(stringResource(R.string.preference_backup_confirm_title)) },
|
|
||||||
text = { Text(stringResource(R.string.preference_backup_confirm_description)) },
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onDismiss) {
|
|
||||||
Text(stringResource(R.string.message_box_cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onEnable) {
|
|
||||||
Text(stringResource(R.string.message_box_continue))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDismissRequest = onDismiss
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun messageFromResult(context: Context, result: Result<Unit>): String {
|
|
||||||
val exception = result.exceptionOrNull()
|
|
||||||
?: return context.getString(R.string.preference_backup_success)
|
|
||||||
|
|
||||||
return when (exception) {
|
|
||||||
is BackupParseException -> when (exception.failureCode) {
|
|
||||||
BackupParseFailure.SAVE_MISSING -> context.getString(R.string.preference_backup_save_missing)
|
|
||||||
BackupParseFailure.DECODE_FAILED -> context.getString(R.string.preference_backup_save_decode_failed)
|
|
||||||
BackupParseFailure.DETAILS_MISSING -> context.getString(R.string.preference_backup_save_details_missing)
|
|
||||||
BackupParseFailure.SAVE_TOO_LARGE -> context.getString(R.string.preference_backup_save_too_large)
|
|
||||||
}
|
|
||||||
is BackupUploadException -> when (exception.failureCode) {
|
|
||||||
BackupUploadFailure.GENERIC_ERROR -> context.getString(R.string.preference_backup_failed, exception.gjCode)
|
|
||||||
BackupUploadFailure.NETWORK_ERROR -> context.getString(R.string.preference_backup_failed_network)
|
|
||||||
BackupUploadFailure.INVALID_LOGIN -> context.getString(R.string.preference_backup_failed_invalid_login)
|
|
||||||
}
|
|
||||||
else -> context.getString(R.string.preference_backup_failed_generic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BackupDialog(onDismiss: () -> Unit, isCancelling: Boolean) {
|
|
||||||
AlertDialog(
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_cloud_upload),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
if (isCancelling) {
|
|
||||||
Text(stringResource(R.string.preference_backup_cancelling))
|
|
||||||
} else {
|
|
||||||
Text(stringResource(R.string.preference_backup_in_progress))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
LinearProgressIndicator()
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onDismiss, enabled = !isCancelling) {
|
|
||||||
Text(stringResource(R.string.message_box_cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDismissRequest = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BackupButton(snackbarHostState: SnackbarHostState) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var showBackupDialog by remember { mutableStateOf(false) }
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
var backupInProgress by remember { mutableStateOf(false) }
|
|
||||||
var backupJob by remember { mutableStateOf<Job?>(null) }
|
|
||||||
|
|
||||||
OptionsButton(
|
|
||||||
title = stringResource(R.string.preference_backup_cloud),
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_cloud_upload),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
showBackupDialog = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showBackupDialog) {
|
|
||||||
BackupConfirmDialog(onDismiss = {
|
|
||||||
showBackupDialog = false
|
|
||||||
}) {
|
|
||||||
showBackupDialog = false
|
|
||||||
|
|
||||||
backupJob = coroutineScope.launch {
|
|
||||||
backupInProgress = true
|
|
||||||
val responseStatus = backupSaveToCloud(context)
|
|
||||||
backupInProgress = false
|
|
||||||
|
|
||||||
snackbarHostState.showSnackbar(messageFromResult(context, responseStatus))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isCancelling by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
if (backupInProgress) {
|
|
||||||
BackupDialog(onDismiss = {
|
|
||||||
coroutineScope.launch {
|
|
||||||
isCancelling = true
|
|
||||||
|
|
||||||
backupJob?.cancelAndJoin()
|
|
||||||
|
|
||||||
isCancelling = false
|
|
||||||
backupInProgress = false
|
|
||||||
}
|
|
||||||
}, isCancelling)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ import android.content.Intent
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.text.format.DateUtils
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
@@ -19,29 +18,13 @@ import androidx.compose.animation.animateColorAsState
|
|||||||
import androidx.compose.animation.core.Spring
|
import androidx.compose.animation.core.Spring
|
||||||
import androidx.compose.animation.core.spring
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
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.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
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.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -54,23 +37,20 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalResources
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.geode.launcher.BuildConfig
|
import com.geode.launcher.BuildConfig
|
||||||
import com.geode.launcher.MainActivity
|
|
||||||
import com.geode.launcher.R
|
import com.geode.launcher.R
|
||||||
import com.geode.launcher.UserDirectoryProvider
|
import com.geode.launcher.UserDirectoryProvider
|
||||||
|
import com.geode.launcher.updater.ReleaseViewModel
|
||||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||||
import com.geode.launcher.ui.theme.LocalTheme
|
import com.geode.launcher.ui.theme.LocalTheme
|
||||||
import com.geode.launcher.ui.theme.Theme
|
import com.geode.launcher.ui.theme.Theme
|
||||||
import com.geode.launcher.updater.ReleaseViewModel
|
|
||||||
import com.geode.launcher.utils.ApplicationIcon
|
import com.geode.launcher.utils.ApplicationIcon
|
||||||
import com.geode.launcher.utils.Constants
|
import com.geode.launcher.utils.Constants
|
||||||
import com.geode.launcher.utils.GamePackageUtils
|
import com.geode.launcher.utils.GamePackageUtils
|
||||||
@@ -80,8 +60,6 @@ import com.geode.launcher.utils.PreferenceUtils
|
|||||||
import java.net.ConnectException
|
import java.net.ConnectException
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.time.Clock
|
|
||||||
|
|
||||||
|
|
||||||
class SettingsActivity : ComponentActivity() {
|
class SettingsActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -131,7 +109,7 @@ fun onOpenFileManager(context: Context) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
} catch (_: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
context.getText(R.string.no_activity_found), Toast.LENGTH_SHORT
|
context.getText(R.string.no_activity_found), Toast.LENGTH_SHORT
|
||||||
@@ -145,7 +123,6 @@ fun UpdateIndicator(
|
|||||||
updateStatus: ReleaseViewModel.ReleaseUIState
|
updateStatus: ReleaseViewModel.ReleaseUIState
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val resources = LocalResources.current
|
|
||||||
|
|
||||||
var enablePopup by remember { mutableStateOf(false) }
|
var enablePopup by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -181,8 +158,8 @@ fun UpdateIndicator(
|
|||||||
is ReleaseViewModel.ReleaseUIState.Failure -> {
|
is ReleaseViewModel.ReleaseUIState.Failure -> {
|
||||||
val message = when (updateStatus.exception) {
|
val message = when (updateStatus.exception) {
|
||||||
is UnknownHostException, is ConnectException ->
|
is UnknownHostException, is ConnectException ->
|
||||||
resources.getString(R.string.release_fetch_no_internet)
|
context.getString(R.string.release_fetch_no_internet)
|
||||||
else -> resources.getString(R.string.preference_check_for_updates_failed)
|
else -> context.getString(R.string.preference_check_for_updates_failed)
|
||||||
}
|
}
|
||||||
|
|
||||||
snackbarHostState.showSnackbar(message)
|
snackbarHostState.showSnackbar(message)
|
||||||
@@ -190,17 +167,17 @@ fun UpdateIndicator(
|
|||||||
is ReleaseViewModel.ReleaseUIState.Finished -> {
|
is ReleaseViewModel.ReleaseUIState.Finished -> {
|
||||||
if (updateStatus.hasUpdated) {
|
if (updateStatus.hasUpdated) {
|
||||||
snackbarHostState.showSnackbar(
|
snackbarHostState.showSnackbar(
|
||||||
resources.getString(R.string.preference_check_for_updates_success)
|
context.getString(R.string.preference_check_for_updates_success)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val gameVersion = GamePackageUtils.getGameVersionCodeOrNull(context.packageManager)
|
val gameVersion = GamePackageUtils.getGameVersionCodeOrNull(context.packageManager)
|
||||||
if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE) {
|
if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE) {
|
||||||
snackbarHostState.showSnackbar(
|
snackbarHostState.showSnackbar(
|
||||||
resources.getString(R.string.launcher_game_update_required_short)
|
context.getString(R.string.launcher_game_update_required_short)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
snackbarHostState.showSnackbar(
|
snackbarHostState.showSnackbar(
|
||||||
resources.getString(R.string.preference_check_for_updates_none_found)
|
context.getString(R.string.preference_check_for_updates_none_found)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,7 +218,7 @@ fun DeveloperModeDialog(onDismiss: () -> Unit, onEnable: () -> Unit) {
|
|||||||
AlertDialog(
|
AlertDialog(
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_warning),
|
Icons.Filled.Warning,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -261,11 +238,73 @@ fun DeveloperModeDialog(onDismiss: () -> Unit, onEnable: () -> Unit) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun GeneralSettingsGroup() {
|
fun SettingsScreen(
|
||||||
|
onBackPressedDispatcher: OnBackPressedDispatcher?,
|
||||||
|
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
|
||||||
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
OptionsGroup(stringResource(R.string.preference_category_testing)) {
|
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,
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.background),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = { innerPadding ->
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
OptionsGroup(context.getString(R.string.preference_category_testing)) {
|
||||||
SettingsSelectCard(
|
SettingsSelectCard(
|
||||||
title = stringResource(R.string.preference_theme_name),
|
title = stringResource(R.string.preference_theme_name),
|
||||||
dialogTitle = stringResource(R.string.preference_theme_select),
|
dialogTitle = stringResource(R.string.preference_theme_select),
|
||||||
@@ -319,16 +358,11 @@ fun GeneralSettingsGroup() {
|
|||||||
onClick = { onOpenFileManager(context) }
|
onClick = { onOpenFileManager(context) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun GameplaySettingsGroup() {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
OptionsGroup(stringResource(R.string.preference_category_gameplay)) {
|
OptionsGroup(stringResource(R.string.preference_category_gameplay)) {
|
||||||
SettingsCard(
|
SettingsCard(
|
||||||
title = stringResource(R.string.preference_load_automatically_name),
|
title = context.getString(R.string.preference_load_automatically_name),
|
||||||
description = stringResource(R.string.preference_load_automatically_description),
|
description = context.getString(R.string.preference_load_automatically_description),
|
||||||
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY,
|
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY,
|
||||||
)
|
)
|
||||||
SettingsSelectCard(
|
SettingsSelectCard(
|
||||||
@@ -373,7 +407,7 @@ fun GameplaySettingsGroup() {
|
|||||||
step = 5,
|
step = 5,
|
||||||
) {
|
) {
|
||||||
SettingsCard(
|
SettingsCard(
|
||||||
title = stringResource(R.string.preference_screen_zoom_fit_name),
|
title = context.getString(R.string.preference_screen_zoom_fit_name),
|
||||||
preferenceKey = PreferenceUtils.Key.SCREEN_ZOOM_FIT,
|
preferenceKey = PreferenceUtils.Key.SCREEN_ZOOM_FIT,
|
||||||
asCard = false
|
asCard = false
|
||||||
)
|
)
|
||||||
@@ -395,51 +429,30 @@ fun GameplaySettingsGroup() {
|
|||||||
maxFrameRate = maxFrameRate
|
maxFrameRate = maxFrameRate
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
OptionsGroup(context.getString(R.string.preference_category_updater)) {
|
||||||
fun UpdateSettingsGroup(releaseViewModel: ReleaseViewModel, snackbarHostState: SnackbarHostState, onUpdate: () -> Unit) {
|
|
||||||
val updateStatus by releaseViewModel.uiState.collectAsState()
|
|
||||||
|
|
||||||
OptionsGroup(stringResource(R.string.preference_category_updater)) {
|
|
||||||
SettingsCard(
|
SettingsCard(
|
||||||
title = stringResource(R.string.preference_update_automatically_name),
|
title = context.getString(R.string.preference_update_automatically_name),
|
||||||
preferenceKey = PreferenceUtils.Key.UPDATE_AUTOMATICALLY,
|
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(
|
OptionsCard(
|
||||||
title = {
|
title = {
|
||||||
OptionsTitle(
|
OptionsTitle(
|
||||||
title = stringResource(R.string.preference_check_for_updates_button),
|
title = stringResource(R.string.preference_check_for_updates_button),
|
||||||
icon = {
|
description = stringResource(
|
||||||
Icon(
|
R.string.preference_check_for_updates_description,
|
||||||
painterResource(R.drawable.icon_update),
|
currentRelease ?: "unknown"
|
||||||
contentDescription = null
|
|
||||||
)
|
)
|
||||||
},
|
|
||||||
description = stringResource(R.string.preference_check_for_updates_description, checkAgoString)
|
|
||||||
.takeIf { lastUpdateCheck != 0L }
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(
|
.clickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
onUpdate()
|
if (releaseViewModel.isInUpdate) {
|
||||||
|
showUpdateInProgress = true
|
||||||
|
} else {
|
||||||
|
releaseViewModel.runReleaseCheck(true)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
role = Role.Button
|
role = Role.Button
|
||||||
)
|
)
|
||||||
@@ -447,11 +460,8 @@ fun UpdateSettingsGroup(releaseViewModel: ReleaseViewModel, snackbarHostState: S
|
|||||||
UpdateIndicator(snackbarHostState, updateStatus)
|
UpdateIndicator(snackbarHostState, updateStatus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
val developerModeEnabled by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
|
||||||
fun AdvancedSettingsGroup(developerModeEnabled: Boolean, snackbarHostState: SnackbarHostState) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
OptionsGroup(stringResource(R.string.preference_category_developer)) {
|
OptionsGroup(stringResource(R.string.preference_category_developer)) {
|
||||||
if (developerModeEnabled) {
|
if (developerModeEnabled) {
|
||||||
@@ -480,43 +490,7 @@ fun AdvancedSettingsGroup(developerModeEnabled: Boolean, snackbarHostState: Snac
|
|||||||
context.startActivity(launchIntent)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BackupButton(snackbarHostState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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)) {
|
OptionsGroup(stringResource(R.string.preference_category_about)) {
|
||||||
var launcherButtonTapped by remember { mutableIntStateOf(0) }
|
var launcherButtonTapped by remember { mutableIntStateOf(0) }
|
||||||
@@ -529,17 +503,13 @@ fun AboutSettingsGroup(developerModeEnabled: Boolean) {
|
|||||||
) {
|
) {
|
||||||
launcherButtonTapped += 1
|
launcherButtonTapped += 1
|
||||||
|
|
||||||
if (launcherButtonTapped >= 7) {
|
if (launcherButtonTapped >= 7 && !developerModeEnabled) {
|
||||||
if (developerModeEnabled) {
|
|
||||||
Toast.makeText(context, R.string.preference_enable_developer_again, Toast.LENGTH_SHORT)
|
|
||||||
.show()
|
|
||||||
} else {
|
|
||||||
showDeveloperDialog = true
|
showDeveloperDialog = true
|
||||||
}
|
|
||||||
launcherButtonTapped = 0
|
launcherButtonTapped = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
if (showDeveloperDialog) {
|
if (showDeveloperDialog) {
|
||||||
DeveloperModeDialog(
|
DeveloperModeDialog(
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
@@ -568,7 +538,7 @@ fun AboutSettingsGroup(developerModeEnabled: Boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val gameSource = remember {
|
val gameSource = remember {
|
||||||
resources.getString(when (GamePackageUtils.identifyGameSource(context.packageManager)) {
|
context.getString(when (GamePackageUtils.identifyGameSource(context.packageManager)) {
|
||||||
GamePackageUtils.GameSource.GOOGLE -> R.string.preference_game_source_google
|
GamePackageUtils.GameSource.GOOGLE -> R.string.preference_game_source_google
|
||||||
GamePackageUtils.GameSource.AMAZON -> R.string.preference_game_source_amazon
|
GamePackageUtils.GameSource.AMAZON -> R.string.preference_game_source_amazon
|
||||||
else -> R.string.preference_game_source_unknown
|
else -> R.string.preference_game_source_unknown
|
||||||
@@ -588,86 +558,6 @@ fun AboutSettingsGroup(developerModeEnabled: Boolean) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun SettingsScreen(
|
|
||||||
onBackPressedDispatcher: OnBackPressedDispatcher?,
|
|
||||||
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
|
|
||||||
) {
|
|
||||||
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, snackbarHostState)
|
|
||||||
AboutSettingsGroup(developerModeEnabled)
|
|
||||||
|
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import androidx.compose.foundation.clickable
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
@@ -20,9 +19,12 @@ import androidx.compose.foundation.selection.toggleable
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
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.AlertDialog
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -46,7 +48,6 @@ import androidx.compose.runtime.staticCompositionLocalOf
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -247,16 +248,12 @@ fun StringDialog(
|
|||||||
) {
|
) {
|
||||||
var enteredValue by remember { mutableStateOf(initialValue) }
|
var enteredValue by remember { mutableStateOf(initialValue) }
|
||||||
|
|
||||||
BaseDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { onDismissRequest() },
|
onDismissRequest = { onDismissRequest() },
|
||||||
title = title,
|
title = {
|
||||||
content = {
|
Text(title)
|
||||||
Column(
|
},
|
||||||
modifier = Modifier
|
text = {
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 12.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = enteredValue,
|
value = enteredValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
@@ -271,15 +268,13 @@ fun StringDialog(
|
|||||||
if (enteredValue.isNotEmpty()) {
|
if (enteredValue.isNotEmpty()) {
|
||||||
IconButton(onClick = { enteredValue = "" }) {
|
IconButton(onClick = { enteredValue = "" }) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_close_small),
|
Icons.Filled.Clear,
|
||||||
contentDescription = stringResource(R.string.preference_text_clear)
|
contentDescription = stringResource(R.string.preference_text_clear)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = { onSelect(enteredValue) }) {
|
TextButton(onClick = { onSelect(enteredValue) }) {
|
||||||
@@ -310,14 +305,17 @@ fun RangeDialog(
|
|||||||
mutableFloatStateOf(initialValue / scale.toFloat())
|
mutableFloatStateOf(initialValue / scale.toFloat())
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { onDismissRequest() },
|
onDismissRequest = { onDismissRequest() },
|
||||||
title = title,
|
title = {
|
||||||
content = {
|
Text(title)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
|
modifier = Modifier.weight(1.0f, false)
|
||||||
) {
|
) {
|
||||||
val precision = log10(scale.toFloat()).roundToInt()
|
val precision = log10(scale.toFloat()).roundToInt()
|
||||||
|
|
||||||
@@ -333,6 +331,8 @@ fun RangeDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
children()
|
children()
|
||||||
|
|
||||||
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = { onSelect((enteredValue * scale).roundToInt()) }) {
|
TextButton(onClick = { onSelect((enteredValue * scale).roundToInt()) }) {
|
||||||
@@ -367,16 +367,16 @@ fun FrameRateDialog(
|
|||||||
val maximumReached = currentValue != null && currentValue > maxFrameRate
|
val maximumReached = currentValue != null && currentValue > maxFrameRate
|
||||||
val minimumReached = currentValue != null && currentValue < minFrameRate
|
val minimumReached = currentValue != null && currentValue < minFrameRate
|
||||||
|
|
||||||
BaseDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { onDismissRequest() },
|
onDismissRequest = { onDismissRequest() },
|
||||||
title = title,
|
title = {
|
||||||
content = {
|
Text(title)
|
||||||
Column(
|
},
|
||||||
modifier = Modifier
|
text = {
|
||||||
.padding(horizontal = 12.dp)
|
Column {
|
||||||
) {
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.weight(1.0f, false)
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
val prevValue = max((currentValue ?: 0) - 5, minFrameRate)
|
val prevValue = max((currentValue ?: 0) - 5, minFrameRate)
|
||||||
@@ -416,7 +416,7 @@ fun FrameRateDialog(
|
|||||||
enteredValue = nextValue.toString()
|
enteredValue = nextValue.toString()
|
||||||
}, enabled = currentValue == null || currentValue < maxFrameRate) {
|
}, enabled = currentValue == null || currentValue < maxFrameRate) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_add),
|
Icons.Filled.Add,
|
||||||
contentDescription = stringResource(R.string.preference_limit_framerate_add)
|
contentDescription = stringResource(R.string.preference_limit_framerate_add)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -629,14 +629,16 @@ fun ProfileCreateDialog(onDismissRequest: () -> Unit) {
|
|||||||
|
|
||||||
val isDuplicate = currentProfiles.contains(filename)
|
val isDuplicate = currentProfiles.contains(filename)
|
||||||
|
|
||||||
BaseDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { onDismissRequest() },
|
onDismissRequest = { onDismissRequest() },
|
||||||
title = stringResource(R.string.preference_profiles_create),
|
title = {
|
||||||
content = {
|
Text(stringResource(R.string.preference_profiles_create))
|
||||||
Column(
|
},
|
||||||
modifier = Modifier
|
text = {
|
||||||
.fillMaxWidth()
|
Column {
|
||||||
.padding(horizontal = 12.dp)
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.weight(1.0f, false)
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = enteredValue,
|
value = enteredValue,
|
||||||
@@ -650,9 +652,9 @@ fun ProfileCreateDialog(onDismissRequest: () -> Unit) {
|
|||||||
label = {
|
label = {
|
||||||
Text(stringResource(R.string.preference_profiles_create_name))
|
Text(stringResource(R.string.preference_profiles_create_name))
|
||||||
},
|
},
|
||||||
isError = isDuplicate,
|
isError = isDuplicate
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (isDuplicate) {
|
if (isDuplicate) {
|
||||||
Spacer(Modifier.size(8.dp))
|
Spacer(Modifier.size(8.dp))
|
||||||
@@ -757,8 +759,8 @@ fun ProfileSelectCard() {
|
|||||||
.setCurrentProfile(selectedProfileId)
|
.setCurrentProfile(selectedProfileId)
|
||||||
|
|
||||||
activity?.run {
|
activity?.run {
|
||||||
packageManager.getLaunchIntentForPackage(packageName)?.also { intent ->
|
packageManager.getLaunchIntentForPackage(packageName)?.also {
|
||||||
val mainIntent = Intent.makeRestartActivityTask(intent.component)
|
val mainIntent = Intent.makeRestartActivityTask(it.component)
|
||||||
startActivity(mainIntent)
|
startActivity(mainIntent)
|
||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
}
|
}
|
||||||
@@ -791,7 +793,7 @@ fun ProfileSelectCard() {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_delete),
|
Icons.Filled.Delete,
|
||||||
contentDescription = stringResource(R.string.preference_profiles_delete_alt)
|
contentDescription = stringResource(R.string.preference_profiles_delete_alt)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -803,54 +805,6 @@ 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
|
@Composable
|
||||||
fun <T> SelectDialog(
|
fun <T> SelectDialog(
|
||||||
title: String,
|
title: String,
|
||||||
@@ -861,20 +815,23 @@ fun <T> SelectDialog(
|
|||||||
) {
|
) {
|
||||||
val (selectedValue, setSelectedValue) = remember { mutableStateOf(initialValue) }
|
val (selectedValue, setSelectedValue) = remember { mutableStateOf(initialValue) }
|
||||||
|
|
||||||
BaseDialog(
|
Dialog(onDismissRequest = onDismissRequest) {
|
||||||
title = title,
|
Card(
|
||||||
onDismissRequest = onDismissRequest,
|
shape = RoundedCornerShape(16.dp)
|
||||||
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(
|
CompositionLocalProvider(
|
||||||
LocalSelectValue provides (selectedValue as Any),
|
LocalSelectValue provides (selectedValue as Any),
|
||||||
LocalSelectSetValue provides ({
|
LocalSelectSetValue provides ({
|
||||||
@@ -884,6 +841,27 @@ fun <T> SelectDialog(
|
|||||||
) {
|
) {
|
||||||
options()
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -940,25 +918,24 @@ fun SettingsCard(title: String, description: String? = null, icon: (@Composable
|
|||||||
icon = icon
|
icon = icon
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier.toggleable(
|
modifier = if (asCard) Modifier.toggleable(
|
||||||
value = settingEnabled.value,
|
value = settingEnabled.value,
|
||||||
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceKey) },
|
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceKey) },
|
||||||
role = Role.Switch,
|
role = Role.Switch,
|
||||||
),
|
) else Modifier
|
||||||
backgroundColor = if (asCard)
|
|
||||||
MaterialTheme.colorScheme.surfaceContainer
|
|
||||||
else MaterialTheme.colorScheme.surfaceContainerHigh
|
|
||||||
) {
|
) {
|
||||||
Switch(
|
Switch(
|
||||||
checked = settingEnabled.value,
|
checked = settingEnabled.value,
|
||||||
onCheckedChange = null
|
onCheckedChange = if (!asCard)
|
||||||
|
{ _ -> settingEnabled.value = toggleSetting(context, preferenceKey) }
|
||||||
|
else null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: String? = null, icon: (@Composable () -> Unit)? = null) {
|
fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: String? = null, icon: (@Composable () -> Unit)? = null) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
icon()
|
icon()
|
||||||
}
|
}
|
||||||
@@ -971,7 +948,7 @@ fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: Stri
|
|||||||
Text(
|
Text(
|
||||||
description,
|
description,
|
||||||
style = Typography.labelMedium,
|
style = Typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -979,20 +956,14 @@ fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OptionsCard(
|
fun OptionsCard(modifier: Modifier = Modifier, wrapContent: Boolean = false, title: @Composable () -> Unit, content: @Composable () -> Unit) {
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
wrapContent: Boolean = false,
|
|
||||||
backgroundColor: Color = MaterialTheme.colorScheme.surfaceContainer,
|
|
||||||
title: @Composable () -> Unit,
|
|
||||||
content: @Composable () -> Unit,
|
|
||||||
) {
|
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 1.dp)
|
.padding(vertical = 1.dp)
|
||||||
.clip(RoundedCornerShape(4.dp))
|
.clip(RoundedCornerShape(4.dp))
|
||||||
.then(modifier)
|
.then(modifier)
|
||||||
.background(backgroundColor)
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||||
.defaultMinSize(minHeight = 64.dp)
|
.defaultMinSize(minHeight = 64.dp)
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
Arrangement.SpaceBetween,
|
Arrangement.SpaceBetween,
|
||||||
@@ -1011,31 +982,6 @@ fun OptionsCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
@Composable
|
||||||
fun InlineText(label: String, icon: @Composable (() -> Unit)? = null, modifier: Modifier = Modifier) {
|
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))
|
LabelledText(label = label, icon = icon, modifier = modifier.padding(horizontal = 16.dp, vertical = 4.dp))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.geode.launcher.ui.theme
|
package com.geode.launcher.ui.theme
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -8,9 +9,14 @@ import androidx.compose.material3.dynamicDarkColorScheme
|
|||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
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(
|
private val DarkColorScheme = darkColorScheme(
|
||||||
primary = Purple80,
|
primary = Purple80,
|
||||||
@@ -22,6 +28,16 @@ private val LightColorScheme = lightColorScheme(
|
|||||||
primary = Purple40,
|
primary = Purple40,
|
||||||
secondary = PurpleGrey40,
|
secondary = PurpleGrey40,
|
||||||
tertiary = Pink40
|
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 {
|
enum class Theme {
|
||||||
|
|||||||
@@ -48,19 +48,8 @@ data class LoaderPayload<T>(
|
|||||||
data class DownloadableAsset(val url: String, val filename: String, val size: Long? = null)
|
data class DownloadableAsset(val url: String, val filename: String, val size: Long? = null)
|
||||||
|
|
||||||
abstract class Downloadable {
|
abstract class Downloadable {
|
||||||
/**
|
|
||||||
* Human-readable representation of the current version.
|
|
||||||
*/
|
|
||||||
abstract fun getDescription(): String
|
abstract fun getDescription(): String
|
||||||
|
|
||||||
/**
|
|
||||||
* Unique value that refers to the current version, used for comparison.
|
|
||||||
*/
|
|
||||||
abstract fun getDescriptor(): Long
|
abstract fun getDescriptor(): Long
|
||||||
|
|
||||||
/**
|
|
||||||
* Release download link for the current platform
|
|
||||||
*/
|
|
||||||
abstract fun getDownload(): DownloadableAsset?
|
abstract fun getDownload(): DownloadableAsset?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,7 @@ import okhttp3.OkHttpClient
|
|||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.InterruptedIOException
|
import java.io.InterruptedIOException
|
||||||
import kotlin.time.Clock
|
|
||||||
import kotlin.time.Duration.Companion.minutes
|
|
||||||
|
|
||||||
private const val TAG_LATEST = "latest"
|
private const val TAG_LATEST = "latest"
|
||||||
private const val TAG_BETA = "prerelease"
|
private const val TAG_BETA = "prerelease"
|
||||||
@@ -85,13 +82,8 @@ class ReleaseManager private constructor(
|
|||||||
val isInUpdate: Boolean
|
val isInUpdate: Boolean
|
||||||
get() = _uiState.value !is ReleaseManagerState.Failure && _uiState.value !is ReleaseManagerState.Finished
|
get() = _uiState.value !is ReleaseManagerState.Failure && _uiState.value !is ReleaseManagerState.Finished
|
||||||
|
|
||||||
private val _availableLauncherUpdateTag = MutableStateFlow<String?>(null)
|
private val _availableLauncherUpdate = MutableStateFlow<DownloadableLauncherRelease?>(null)
|
||||||
val availableLauncherUpdateTag = _availableLauncherUpdateTag.asStateFlow()
|
val availableLauncherUpdate = _availableLauncherUpdate.asStateFlow()
|
||||||
|
|
||||||
private val _availableLauncherUpdateDetails = MutableStateFlow<DownloadableLauncherRelease?>(null)
|
|
||||||
val availableLauncherUpdateDetails = _availableLauncherUpdateDetails.asStateFlow()
|
|
||||||
|
|
||||||
var dismissedLauncherUpdate = false
|
|
||||||
|
|
||||||
private fun sendError(e: Exception) {
|
private fun sendError(e: Exception) {
|
||||||
_uiState.value = ReleaseManagerState.Failure(e)
|
_uiState.value = ReleaseManagerState.Failure(e)
|
||||||
@@ -139,27 +131,32 @@ class ReleaseManager private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun downloadLauncherUpdate(release: Downloadable): Result<File> {
|
private suspend fun downloadLauncherUpdate(release: Downloadable) {
|
||||||
val download = release.getDownload()
|
val download = release.getDownload() ?: return
|
||||||
?: return Result.failure(FileNotFoundException())
|
|
||||||
|
|
||||||
val outputDirectory = applicationContext.filesDir
|
val outputDirectory = LaunchUtils.getBaseDirectory(applicationContext)
|
||||||
val outputFile = File(outputDirectory, download.filename)
|
val outputFile = File(outputDirectory, download.filename)
|
||||||
|
|
||||||
try {
|
if (outputFile.exists()) {
|
||||||
DownloadUtils.downloadStream(
|
// only download the apk once
|
||||||
httpClient,
|
return
|
||||||
download.url
|
|
||||||
).use { fileStream ->
|
|
||||||
outputFile.outputStream().use {
|
|
||||||
fileStream.copyTo(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return Result.failure(e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.success(outputFile)
|
_uiState.value = ReleaseManagerState.InDownload(0, download.size)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val fileStream = DownloadUtils.downloadStream(
|
||||||
|
httpClient,
|
||||||
|
download.url
|
||||||
|
) { progress, outOf ->
|
||||||
|
_uiState.value = ReleaseManagerState.InDownload(progress, outOf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileStream.copyTo(outputFile.outputStream())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
sendError(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun performUpdate(release: Downloadable) {
|
private suspend fun performUpdate(release: Downloadable) {
|
||||||
@@ -212,6 +209,8 @@ class ReleaseManager private constructor(
|
|||||||
} catch (_: Exception) { }
|
} catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadLauncherUpdateIfNecessary()
|
||||||
|
|
||||||
// extraction performed
|
// extraction performed
|
||||||
updatePreferences(release)
|
updatePreferences(release)
|
||||||
_uiState.value = ReleaseManagerState.Finished(true)
|
_uiState.value = ReleaseManagerState.Finished(true)
|
||||||
@@ -234,73 +233,18 @@ class ReleaseManager private constructor(
|
|||||||
return originalFileHash != currentFileHash
|
return originalFileHash != currentFileHash
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getLatestLauncherUpdate(): DownloadableLauncherRelease? {
|
private fun checkLauncherUpdate(launcherUpdate: DownloadableLauncherRelease) {
|
||||||
return try {
|
if (launcherUpdate.release.tagName != BuildConfig.VERSION_NAME) {
|
||||||
releaseRepository.getLatestLauncherRelease()
|
_availableLauncherUpdate.value = launcherUpdate
|
||||||
} catch (e: Exception) {
|
|
||||||
sendError(e)
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun checkLauncherUpdate() {
|
private suspend fun downloadLauncherUpdateIfNecessary() {
|
||||||
val launcherRelease = getLatestLauncherUpdate() ?: return
|
val update = _availableLauncherUpdate.value ?: return
|
||||||
|
downloadLauncherUpdate(update)
|
||||||
// unconditionally track it, we're not using it for checks and it might be good to have
|
|
||||||
_availableLauncherUpdateDetails.value = launcherRelease
|
|
||||||
|
|
||||||
val launcherTag = launcherRelease.release.tagName
|
|
||||||
if (launcherTag != BuildConfig.VERSION_NAME) {
|
|
||||||
_availableLauncherUpdateTag.value = launcherTag
|
|
||||||
|
|
||||||
PreferenceUtils.get(applicationContext)
|
|
||||||
.setString(PreferenceUtils.Key.LAST_LAUNCHER_UPDATE, launcherTag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkCachedLauncherUpdate() {
|
|
||||||
val lastUpdate = PreferenceUtils.get(applicationContext)
|
|
||||||
.getString(PreferenceUtils.Key.LAST_LAUNCHER_UPDATE) ?: return
|
|
||||||
|
|
||||||
if (lastUpdate != BuildConfig.VERSION_NAME) {
|
|
||||||
_availableLauncherUpdateTag.value = lastUpdate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun downloadLatestLauncherUpdate(): Result<File> {
|
|
||||||
val update = _availableLauncherUpdateDetails.value
|
|
||||||
?: getLatestLauncherUpdate()
|
|
||||||
?: return Result.failure(FileNotFoundException())
|
|
||||||
|
|
||||||
return downloadLauncherUpdate(update)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun shouldUseCache(): Boolean {
|
|
||||||
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
|
||||||
if (sharedPreferences.getBoolean(PreferenceUtils.Key.DISABLE_UPDATE_CACHE)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
return lastCheckTime > checkMinTime && !fileWasExternallyModified()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun afterUseCachedUpdate() {
|
|
||||||
checkCachedLauncherUpdate()
|
|
||||||
_uiState.tryEmit(ReleaseManagerState.Finished())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkForNewRelease(isManual: Boolean = false) {
|
private suspend fun checkForNewRelease(isManual: Boolean = false) {
|
||||||
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
|
||||||
|
|
||||||
if (!isManual && shouldUseCache()) {
|
|
||||||
afterUseCachedUpdate()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val release = try {
|
val release = try {
|
||||||
getLatestRelease()
|
getLatestRelease()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -308,21 +252,33 @@ class ReleaseManager private constructor(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
checkLauncherUpdate()
|
val launcherRelease = try {
|
||||||
|
releaseRepository.getLatestLauncherRelease()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
sendError(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val currentTime = Clock.System.now().toEpochMilliseconds()
|
if (launcherRelease != null) {
|
||||||
sharedPreferences.setLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME, currentTime)
|
checkLauncherUpdate(launcherRelease)
|
||||||
|
}
|
||||||
|
|
||||||
if (release == null) {
|
if (release == null) {
|
||||||
|
downloadLauncherUpdateIfNecessary()
|
||||||
|
|
||||||
_uiState.value = ReleaseManagerState.Finished()
|
_uiState.value = ReleaseManagerState.Finished()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
||||||
|
|
||||||
val currentVersion = sharedPreferences.getLong(PreferenceUtils.Key.CURRENT_VERSION_TIMESTAMP)
|
val currentVersion = sharedPreferences.getLong(PreferenceUtils.Key.CURRENT_VERSION_TIMESTAMP)
|
||||||
val latestVersion = release.getDescriptor()
|
val latestVersion = release.getDescriptor()
|
||||||
|
|
||||||
// check if an update is needed
|
// check if an update is needed
|
||||||
if (latestVersion == currentVersion && !fileWasExternallyModified()) {
|
if (latestVersion == currentVersion && !fileWasExternallyModified()) {
|
||||||
|
downloadLauncherUpdateIfNecessary()
|
||||||
|
|
||||||
_uiState.value = ReleaseManagerState.Finished()
|
_uiState.value = ReleaseManagerState.Finished()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -331,6 +287,8 @@ class ReleaseManager private constructor(
|
|||||||
|
|
||||||
// check if the file was externally modified
|
// check if the file was externally modified
|
||||||
if (!allowOverwriting && fileWasExternallyModified(true)) {
|
if (!allowOverwriting && fileWasExternallyModified(true)) {
|
||||||
|
downloadLauncherUpdateIfNecessary()
|
||||||
|
|
||||||
sendError(UpdateException(UpdateException.Reason.EXTERNAL_FILE_IN_USE))
|
sendError(UpdateException(UpdateException.Reason.EXTERNAL_FILE_IN_USE))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -359,7 +317,7 @@ class ReleaseManager private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun computeFileHash(file: File): String =
|
private fun computeFileHash(file: File): String =
|
||||||
file.source().buffer().use { it.readByteString().md5().hex() }
|
file.source().buffer().readByteString().md5().hex()
|
||||||
|
|
||||||
private fun getGeodeOutputPath(): File {
|
private fun getGeodeOutputPath(): File {
|
||||||
val geodeName = LaunchUtils.geodeFilename
|
val geodeName = LaunchUtils.geodeFilename
|
||||||
@@ -394,14 +352,10 @@ class ReleaseManager private constructor(
|
|||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
fun checkForUpdates(isManual: Boolean = false): StateFlow<ReleaseManagerState> {
|
fun checkForUpdates(isManual: Boolean = false): StateFlow<ReleaseManagerState> {
|
||||||
if (!isInUpdate) {
|
if (!isInUpdate) {
|
||||||
if (isManual || !shouldUseCache()) {
|
|
||||||
_uiState.value = ReleaseManagerState.InUpdateCheck
|
_uiState.value = ReleaseManagerState.InUpdateCheck
|
||||||
updateJob = GlobalScope.launch {
|
updateJob = GlobalScope.launch {
|
||||||
checkForNewRelease(isManual)
|
checkForNewRelease(isManual)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
afterUseCachedUpdate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return _uiState.asStateFlow()
|
return _uiState.asStateFlow()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.geode.launcher.updater
|
package com.geode.launcher.updater
|
||||||
|
|
||||||
import com.geode.launcher.BuildConfig
|
import com.geode.launcher.utils.DownloadUtils.executeCoroutine
|
||||||
import com.geode.launcher.utils.LaunchUtils
|
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlinx.datetime.DateTimeUnit
|
import kotlinx.datetime.DateTimeUnit
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
@@ -11,10 +10,8 @@ import kotlinx.serialization.json.Json
|
|||||||
import kotlinx.serialization.json.JsonNamingStrategy
|
import kotlinx.serialization.json.JsonNamingStrategy
|
||||||
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.OkHttp
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.coroutines.executeAsync
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
@@ -31,8 +28,6 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
|||||||
private const val GEODE_API_BASE = "https://api.geode-sdk.org/v1"
|
private const val GEODE_API_BASE = "https://api.geode-sdk.org/v1"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val userAgent = "okhttp/${OkHttp.VERSION} (launcher=${BuildConfig.VERSION_NAME};arch=${LaunchUtils.applicationArchitecture})"
|
|
||||||
|
|
||||||
suspend fun getLatestLauncherRelease(): DownloadableLauncherRelease? {
|
suspend fun getLatestLauncherRelease(): DownloadableLauncherRelease? {
|
||||||
val releasePath = "$GITHUB_API_BASE/repos/geode-sdk/android-launcher/releases/latest"
|
val releasePath = "$GITHUB_API_BASE/repos/geode-sdk/android-launcher/releases/latest"
|
||||||
|
|
||||||
@@ -64,13 +59,12 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
|||||||
.url(url)
|
.url(url)
|
||||||
.addHeader("Accept", "application/json")
|
.addHeader("Accept", "application/json")
|
||||||
.addHeader(GITHUB_API_HEADER, GITHUB_API_VERSION)
|
.addHeader(GITHUB_API_HEADER, GITHUB_API_VERSION)
|
||||||
.addHeader("User-Agent", userAgent)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val call = httpClient.newCall(request)
|
val call = httpClient.newCall(request)
|
||||||
|
val response = call.executeCoroutine()
|
||||||
|
|
||||||
return call.executeAsync().use { response ->
|
return when (response.code) {
|
||||||
when (response.code) {
|
|
||||||
200 -> {
|
200 -> {
|
||||||
val format = Json {
|
val format = Json {
|
||||||
namingStrategy = JsonNamingStrategy.SnakeCase
|
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||||
@@ -78,7 +72,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val release = format.decodeFromBufferedSource<List<Release>>(
|
val release = format.decodeFromBufferedSource<List<Release>>(
|
||||||
response.body.source()
|
response.body!!.source()
|
||||||
)
|
)
|
||||||
|
|
||||||
(release.find { it.tagName != "nightly" })
|
(release.find { it.tagName != "nightly" })
|
||||||
@@ -100,7 +94,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
|||||||
throw IOException(msg)
|
throw IOException(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
throw IOException("response 403: ${response.body.string()}")
|
throw IOException("response 403: ${response.body!!.string()}")
|
||||||
}
|
}
|
||||||
404 -> {
|
404 -> {
|
||||||
null
|
null
|
||||||
@@ -110,7 +104,6 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
@OptIn(ExperimentalTime::class)
|
||||||
private fun generateRateLimitMessage(resetTime: Instant): String {
|
private fun generateRateLimitMessage(resetTime: Instant): String {
|
||||||
@@ -128,12 +121,12 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
|||||||
.url(url)
|
.url(url)
|
||||||
.addHeader("Accept", "application/json")
|
.addHeader("Accept", "application/json")
|
||||||
.addHeader(GITHUB_API_HEADER, GITHUB_API_VERSION)
|
.addHeader(GITHUB_API_HEADER, GITHUB_API_VERSION)
|
||||||
.addHeader("User-Agent", userAgent)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val call = httpClient.newCall(request)
|
val call = httpClient.newCall(request)
|
||||||
return call.executeAsync().use { response ->
|
val response = call.executeCoroutine()
|
||||||
when (response.code) {
|
|
||||||
|
return when (response.code) {
|
||||||
200 -> {
|
200 -> {
|
||||||
val format = Json {
|
val format = Json {
|
||||||
namingStrategy = JsonNamingStrategy.SnakeCase
|
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||||
@@ -141,7 +134,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val release = format.decodeFromBufferedSource<Release>(
|
val release = format.decodeFromBufferedSource<Release>(
|
||||||
response.body.source()
|
response.body!!.source()
|
||||||
)
|
)
|
||||||
|
|
||||||
release
|
release
|
||||||
@@ -162,7 +155,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
|||||||
throw IOException(msg)
|
throw IOException(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
throw IOException("response 403: ${response.body.string()}")
|
throw IOException("response 403: ${response.body!!.string()}")
|
||||||
}
|
}
|
||||||
404 -> {
|
404 -> {
|
||||||
null
|
null
|
||||||
@@ -172,7 +165,6 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
suspend fun getLatestIndexRelease(gd: Long, prerelease: Boolean = false): DownloadableLoaderRelease? {
|
suspend fun getLatestIndexRelease(gd: Long, prerelease: Boolean = false): DownloadableLoaderRelease? {
|
||||||
@@ -186,12 +178,12 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
|||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.addHeader("Accept", "application/json")
|
.addHeader("Accept", "application/json")
|
||||||
.addHeader("User-Agent", userAgent)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val call = httpClient.newCall(request)
|
val call = httpClient.newCall(request)
|
||||||
return call.executeAsync().use { response ->
|
val response = call.executeCoroutine()
|
||||||
when (response.code) {
|
|
||||||
|
return when (response.code) {
|
||||||
200 -> {
|
200 -> {
|
||||||
val format = Json {
|
val format = Json {
|
||||||
namingStrategy = JsonNamingStrategy.SnakeCase
|
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||||
@@ -199,7 +191,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val body = format.decodeFromBufferedSource<LoaderPayload<LoaderVersion>>(
|
val body = format.decodeFromBufferedSource<LoaderPayload<LoaderVersion>>(
|
||||||
response.body.source()
|
response.body!!.source()
|
||||||
)
|
)
|
||||||
|
|
||||||
val release = body.payload ?: throw IOException("index error: ${body.error}")
|
val release = body.payload ?: throw IOException("index error: ${body.error}")
|
||||||
@@ -215,4 +207,3 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -53,7 +53,7 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
|
|||||||
val isInUpdate
|
val isInUpdate
|
||||||
get() = ReleaseManager.get(application).isInUpdate
|
get() = ReleaseManager.get(application).isInUpdate
|
||||||
|
|
||||||
val nextLauncherUpdate = ReleaseManager.get(application).availableLauncherUpdateTag
|
val nextLauncherUpdate = ReleaseManager.get(application).availableLauncherUpdate
|
||||||
|
|
||||||
var hasPerformedCheck = false
|
var hasPerformedCheck = false
|
||||||
private set
|
private set
|
||||||
|
|||||||
@@ -3,18 +3,17 @@ package com.geode.launcher.utils
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.FileUtils
|
import android.os.FileUtils
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Callback
|
||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import okhttp3.coroutines.executeAsync
|
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import okio.BufferedSink
|
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
import okio.ForwardingSink
|
|
||||||
import okio.ForwardingSource
|
import okio.ForwardingSource
|
||||||
import okio.Sink
|
|
||||||
import okio.Source
|
import okio.Source
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -22,6 +21,8 @@ import java.io.InputStream
|
|||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
|
|
||||||
typealias ProgressCallback = (progress: Long, outOf: Long) -> Unit
|
typealias ProgressCallback = (progress: Long, outOf: Long) -> Unit
|
||||||
@@ -49,7 +50,7 @@ object DownloadUtils {
|
|||||||
val originalResponse = chain.proceed(chain.request())
|
val originalResponse = chain.proceed(chain.request())
|
||||||
originalResponse.newBuilder()
|
originalResponse.newBuilder()
|
||||||
.body(ProgressResponseBody(
|
.body(ProgressResponseBody(
|
||||||
originalResponse.body, onProgress
|
originalResponse.body!!, onProgress
|
||||||
))
|
))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
@@ -58,9 +59,31 @@ object DownloadUtils {
|
|||||||
val progressClient = progressClientBuilder.build()
|
val progressClient = progressClientBuilder.build()
|
||||||
|
|
||||||
val call = progressClient.newCall(request)
|
val call = progressClient.newCall(request)
|
||||||
val response = call.executeAsync()
|
val response = call.executeCoroutine()
|
||||||
|
|
||||||
return response.body.byteStream()
|
return response.body!!.byteStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun Call.executeCoroutine(): Response {
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
this.enqueue(object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
if (continuation.isCancelled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation.resumeWithException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
continuation.resume(response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
this.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun extractFileFromZipStream(inputStream: InputStream, outputStream: OutputStream, zipPath: String) {
|
suspend fun extractFileFromZipStream(inputStream: InputStream, outputStream: OutputStream, zipPath: String) {
|
||||||
@@ -144,38 +167,3 @@ private class ProgressResponseBody (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// now based on <https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/UploadProgress.java#L84>
|
|
||||||
class ProgressRequestBody(
|
|
||||||
private val requestBody: RequestBody,
|
|
||||||
private val progressCallback: ProgressCallback
|
|
||||||
) : RequestBody() {
|
|
||||||
override fun contentType(): MediaType? {
|
|
||||||
return requestBody.contentType()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun contentLength(): Long {
|
|
||||||
return requestBody.contentLength()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun writeTo(sink: BufferedSink) {
|
|
||||||
val bufferedSink = this.sink(sink).buffer()
|
|
||||||
requestBody.writeTo(bufferedSink)
|
|
||||||
bufferedSink.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sink(sink: Sink): Sink {
|
|
||||||
return object : ForwardingSink(sink) {
|
|
||||||
var totalBytesWritten = 0L
|
|
||||||
|
|
||||||
override fun write(source: Buffer, byteCount: Long) {
|
|
||||||
super.write(source, byteCount)
|
|
||||||
totalBytesWritten += byteCount
|
|
||||||
progressCallback(
|
|
||||||
totalBytesWritten,
|
|
||||||
contentLength()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -51,8 +51,7 @@ object GamePackageUtils {
|
|||||||
37L to "2.200",
|
37L to "2.200",
|
||||||
38L to "2.205",
|
38L to "2.205",
|
||||||
39L to "2.206",
|
39L to "2.206",
|
||||||
40L to "2.2074", // ok
|
40L to "2.2074" // ok
|
||||||
41L to "2.2081",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -394,15 +394,6 @@ object GeodeUtils {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getGameVersionCode(): Long {
|
|
||||||
activity.get()?.run {
|
|
||||||
return GamePackageUtils.getGameVersionCode(packageManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current version code of the launcher.
|
* Returns the current version code of the launcher.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -127,33 +127,34 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
enum class Key {
|
enum class Key {
|
||||||
LOAD_AUTOMATICALLY,
|
LOAD_AUTOMATICALLY,
|
||||||
UPDATE_AUTOMATICALLY,
|
UPDATE_AUTOMATICALLY,
|
||||||
|
RELEASE_CHANNEL,
|
||||||
CURRENT_VERSION_TAG,
|
CURRENT_VERSION_TAG,
|
||||||
CURRENT_VERSION_TIMESTAMP,
|
CURRENT_VERSION_TIMESTAMP,
|
||||||
THEME,
|
THEME,
|
||||||
BLACK_BACKGROUND,
|
BLACK_BACKGROUND,
|
||||||
CURRENT_RELEASE_MODIFIED,
|
CURRENT_RELEASE_MODIFIED,
|
||||||
|
LAST_DISMISSED_UPDATE,
|
||||||
DISMISSED_GJ_UPDATE,
|
DISMISSED_GJ_UPDATE,
|
||||||
LAUNCH_ARGUMENTS,
|
LAUNCH_ARGUMENTS,
|
||||||
LIMIT_ASPECT_RATIO,
|
LIMIT_ASPECT_RATIO,
|
||||||
DISPLAY_MODE,
|
DISPLAY_MODE,
|
||||||
|
FORCE_HRR,
|
||||||
|
ENABLE_REDESIGN,
|
||||||
RELEASE_CHANNEL_TAG,
|
RELEASE_CHANNEL_TAG,
|
||||||
DEVELOPER_MODE,
|
DEVELOPER_MODE,
|
||||||
CLEANUP_APKS,
|
CLEANUP_APKS,
|
||||||
|
CUSTOM_SYMBOL_LIST,
|
||||||
CUSTOM_ASPECT_RATIO,
|
CUSTOM_ASPECT_RATIO,
|
||||||
SCREEN_ZOOM,
|
SCREEN_ZOOM,
|
||||||
SCREEN_ZOOM_FIT,
|
SCREEN_ZOOM_FIT,
|
||||||
LIMIT_FRAME_RATE,
|
LIMIT_FRAME_RATE,
|
||||||
DISABLE_USER_THEME,
|
DISABLE_USER_THEME,
|
||||||
SELECTED_ICON,
|
SELECTED_ICON
|
||||||
WAIT_PERIOD,
|
|
||||||
LAST_UPDATE_CHECK_TIME,
|
|
||||||
LAST_LAUNCHER_UPDATE,
|
|
||||||
DISABLE_UPDATE_CACHE,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
||||||
return when (key) {
|
return when (key) {
|
||||||
Key.UPDATE_AUTOMATICALLY -> true
|
Key.UPDATE_AUTOMATICALLY, Key.FORCE_HRR -> true
|
||||||
Key.DEVELOPER_MODE -> BuildConfig.DEBUG
|
Key.DEVELOPER_MODE -> BuildConfig.DEBUG
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
@@ -162,7 +163,6 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
private fun defaultValueForIntKey(key: Key) = when (key) {
|
private fun defaultValueForIntKey(key: Key) = when (key) {
|
||||||
Key.DISPLAY_MODE -> if (this.getBoolean(Key.LIMIT_ASPECT_RATIO)) 1 else 0
|
Key.DISPLAY_MODE -> if (this.getBoolean(Key.LIMIT_ASPECT_RATIO)) 1 else 0
|
||||||
Key.SCREEN_ZOOM -> 100
|
Key.SCREEN_ZOOM -> 100
|
||||||
Key.WAIT_PERIOD -> 3
|
|
||||||
// people wanted a reset on nightly anyways, so this is a good excuse to do so
|
// people wanted a reset on nightly anyways, so this is a good excuse to do so
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
@@ -177,33 +177,29 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
return when (key) {
|
return when (key) {
|
||||||
Key.LOAD_AUTOMATICALLY -> "PreferenceLoadAutomatically"
|
Key.LOAD_AUTOMATICALLY -> "PreferenceLoadAutomatically"
|
||||||
Key.UPDATE_AUTOMATICALLY -> "PreferenceUpdateAutomatically"
|
Key.UPDATE_AUTOMATICALLY -> "PreferenceUpdateAutomatically"
|
||||||
|
Key.RELEASE_CHANNEL -> "PreferenceReleaseChannel"
|
||||||
Key.CURRENT_VERSION_TAG -> "PreferenceCurrentVersionName"
|
Key.CURRENT_VERSION_TAG -> "PreferenceCurrentVersionName"
|
||||||
Key.CURRENT_VERSION_TIMESTAMP -> "PreferenceCurrentVersionDescriptor"
|
Key.CURRENT_VERSION_TIMESTAMP -> "PreferenceCurrentVersionDescriptor"
|
||||||
Key.THEME -> "PreferenceTheme"
|
Key.THEME -> "PreferenceTheme"
|
||||||
Key.BLACK_BACKGROUND -> "PreferenceBlackBackground"
|
Key.BLACK_BACKGROUND -> "PreferenceBlackBackground"
|
||||||
Key.CURRENT_RELEASE_MODIFIED -> "PreferenceReleaseModifiedHash"
|
Key.CURRENT_RELEASE_MODIFIED -> "PreferenceReleaseModifiedHash"
|
||||||
|
Key.LAST_DISMISSED_UPDATE -> "PreferenceLastDismissedUpdate"
|
||||||
Key.DISMISSED_GJ_UPDATE -> "PreferenceDismissedGJUpdate"
|
Key.DISMISSED_GJ_UPDATE -> "PreferenceDismissedGJUpdate"
|
||||||
Key.LAUNCH_ARGUMENTS -> "PreferenceLaunchArguments"
|
Key.LAUNCH_ARGUMENTS -> "PreferenceLaunchArguments"
|
||||||
Key.LIMIT_ASPECT_RATIO -> "PreferenceLimitAspectRatio"
|
Key.LIMIT_ASPECT_RATIO -> "PreferenceLimitAspectRatio"
|
||||||
Key.DISPLAY_MODE -> "PreferenceDisplayMode"
|
Key.DISPLAY_MODE -> "PreferenceDisplayMode"
|
||||||
|
Key.FORCE_HRR -> "PreferenceForceHighRefreshRate"
|
||||||
|
Key.ENABLE_REDESIGN -> "PreferenceEnableRedesign"
|
||||||
Key.RELEASE_CHANNEL_TAG -> "PreferenceReleaseChannelTag"
|
Key.RELEASE_CHANNEL_TAG -> "PreferenceReleaseChannelTag"
|
||||||
Key.DEVELOPER_MODE -> "PreferenceDeveloperMode"
|
Key.DEVELOPER_MODE -> "PreferenceDeveloperMode"
|
||||||
Key.CLEANUP_APKS -> "PreferenceCleanupPackages"
|
Key.CLEANUP_APKS -> "PreferenceCleanupPackages"
|
||||||
|
Key.CUSTOM_SYMBOL_LIST -> "PreferenceCustomSymbolList"
|
||||||
Key.CUSTOM_ASPECT_RATIO -> "PreferenceCustomAspectRatio"
|
Key.CUSTOM_ASPECT_RATIO -> "PreferenceCustomAspectRatio"
|
||||||
Key.SCREEN_ZOOM -> "PreferenceScreenZoom"
|
Key.SCREEN_ZOOM -> "PreferenceScreenZoom"
|
||||||
Key.SCREEN_ZOOM_FIT -> "PreferenceScreenZoomFit"
|
Key.SCREEN_ZOOM_FIT -> "PreferenceScreenZoomFit"
|
||||||
Key.LIMIT_FRAME_RATE -> "PreferenceLimitFrameRate"
|
Key.LIMIT_FRAME_RATE -> "PreferenceLimitFrameRate"
|
||||||
Key.DISABLE_USER_THEME -> "PreferenceDisableUserTheme"
|
Key.DISABLE_USER_THEME -> "PreferenceDisableUserTheme"
|
||||||
Key.SELECTED_ICON -> "PreferenceSelectedIcon"
|
Key.SELECTED_ICON -> "PreferenceSelectedIcon"
|
||||||
Key.WAIT_PERIOD -> "PreferenceWaitPeriod"
|
|
||||||
Key.LAST_UPDATE_CHECK_TIME -> "PreferenceLastUpdateCheckTime"
|
|
||||||
Key.LAST_LAUNCHER_UPDATE -> "PreferenceLastLauncherUpdate"
|
|
||||||
Key.DISABLE_UPDATE_CACHE -> "PreferenceDisableUpdateCache"
|
|
||||||
// Key.RELEASE_CHANNEL -> "PreferenceReleaseChannel"
|
|
||||||
// Key.LAST_DISMISSED_UPDATE -> "PreferenceLastDismissedUpdate"
|
|
||||||
// Key.FORCE_HRR -> "PreferenceForceHighRefreshRate"
|
|
||||||
// Key.DISABLE_REDESIGN -> "PreferenceDisableRedesign"
|
|
||||||
// Key.CUSTOM_SYMBOL_LIST -> "PreferenceCustomSymbolList"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import kotlin.math.abs
|
|||||||
private const val HANDLER_OPEN_IME_KEYBOARD = 2
|
private const val HANDLER_OPEN_IME_KEYBOARD = 2
|
||||||
private const val HANDLER_CLOSE_IME_KEYBOARD = 3
|
private const val HANDLER_CLOSE_IME_KEYBOARD = 3
|
||||||
private const val MS_TO_NS = 1_000_000
|
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) {
|
class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -168,7 +167,6 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
val ys = FloatArray(pointerNumber)
|
val ys = FloatArray(pointerNumber)
|
||||||
val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
motionEvent.eventTimeNanos else motionEvent.eventTime * MS_TO_NS
|
motionEvent.eventTimeNanos else motionEvent.eventTime * MS_TO_NS
|
||||||
val timestampSec = timestamp / SEC_TO_NS
|
|
||||||
|
|
||||||
for (i in 0 until pointerNumber) {
|
for (i in 0 until pointerNumber) {
|
||||||
ids[i] = motionEvent.getPointerId(i)
|
ids[i] = motionEvent.getPointerId(i)
|
||||||
@@ -190,7 +188,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
GeodeUtils.TOUCH_TYPE_BEGIN, ids.sliceArray(0..0), xs.sliceArray(0..0), ys.sliceArray(0..0)
|
GeodeUtils.TOUCH_TYPE_BEGIN, ids.sliceArray(0..0), xs.sliceArray(0..0), ys.sliceArray(0..0)
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
cocos2dxRenderer.handleActionDown(idDown, xDown, f, timestampSec)
|
cocos2dxRenderer.handleActionDown(idDown, xDown, f)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -207,7 +205,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
GeodeUtils.TOUCH_TYPE_END, ids.sliceArray(0..0), xs.sliceArray(0..0), ys.sliceArray(0..0)
|
GeodeUtils.TOUCH_TYPE_END, ids.sliceArray(0..0), xs.sliceArray(0..0), ys.sliceArray(0..0)
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
cocos2dxRenderer.handleActionUp(idUp, f2, f3, timestampSec)
|
cocos2dxRenderer.handleActionUp(idUp, f2, f3)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -221,7 +219,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
GeodeUtils.TOUCH_TYPE_MOVE, ids, xs, ys
|
GeodeUtils.TOUCH_TYPE_MOVE, ids, xs, ys
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
cocos2dxRenderer.handleActionMove(ids, xs, ys, timestampSec)
|
cocos2dxRenderer.handleActionMove(ids, xs, ys)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -235,7 +233,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
GeodeUtils.TOUCH_TYPE_CANCEL, ids, xs, ys
|
GeodeUtils.TOUCH_TYPE_CANCEL, ids, xs, ys
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
cocos2dxRenderer.handleActionCancel(ids, xs, ys, timestampSec)
|
cocos2dxRenderer.handleActionCancel(ids, xs, ys)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -253,7 +251,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
GeodeUtils.TOUCH_TYPE_BEGIN, intArrayOf(idPointerDown), floatArrayOf(xPointerDown), floatArrayOf(y)
|
GeodeUtils.TOUCH_TYPE_BEGIN, intArrayOf(idPointerDown), floatArrayOf(xPointerDown), floatArrayOf(y)
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
cocos2dxRenderer.handleActionDown(idPointerDown, xPointerDown, y, timestampSec)
|
cocos2dxRenderer.handleActionDown(idPointerDown, xPointerDown, y)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -271,7 +269,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
GeodeUtils.TOUCH_TYPE_END, intArrayOf(idPointerUp), floatArrayOf(xPointerUp), floatArrayOf(y2)
|
GeodeUtils.TOUCH_TYPE_END, intArrayOf(idPointerUp), floatArrayOf(xPointerUp), floatArrayOf(y2)
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
cocos2dxRenderer.handleActionUp(idPointerUp, xPointerUp, y2, timestampSec)
|
cocos2dxRenderer.handleActionUp(idPointerUp, xPointerUp, y2)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,25 +40,21 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun nativeTextClosed()
|
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
|
@JvmStatic
|
||||||
private external fun nativeTouchesBegin(id: Int, x: Float, y: Float, timestamp: Double)
|
private external fun nativeTouchesBegin(id: Int, x: Float, y: Float)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun nativeTouchesCancel(
|
private external fun nativeTouchesCancel(
|
||||||
ids: IntArray,
|
ids: IntArray,
|
||||||
xs: FloatArray,
|
xs: FloatArray,
|
||||||
ys: FloatArray,
|
ys: FloatArray
|
||||||
timestamp: Double,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun nativeTouchesEnd(id: Int, x: Float, y: Float, timestamp: Double)
|
private external fun nativeTouchesEnd(id: Int, x: Float, y: Float)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun nativeTouchesMove(ids: IntArray, xs: FloatArray, ys: FloatArray, timestamp: Double)
|
private external fun nativeTouchesMove(ids: IntArray, xs: FloatArray, ys: FloatArray)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun nativeInit(width: Int, height: Int)
|
private external fun nativeInit(width: Int, height: Int)
|
||||||
@@ -113,20 +109,20 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleActionDown(id: Int, x: Float, y: Float, timestamp: Double) {
|
fun handleActionDown(id: Int, x: Float, y: Float) {
|
||||||
nativeTouchesBegin(id, x, y, timestamp)
|
nativeTouchesBegin(id, x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleActionUp(id: Int, x: Float, y: Float, timestamp: Double) {
|
fun handleActionUp(id: Int, x: Float, y: Float) {
|
||||||
nativeTouchesEnd(id, x, y, timestamp)
|
nativeTouchesEnd(id, x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleActionCancel(ids: IntArray, xs: FloatArray, ys: FloatArray, timestamp: Double) {
|
fun handleActionCancel(ids: IntArray, xs: FloatArray, ys: FloatArray) {
|
||||||
nativeTouchesCancel(ids, xs, ys, timestamp)
|
nativeTouchesCancel(ids, xs, ys)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleActionMove(ids: IntArray, xs: FloatArray, ys: FloatArray, timestamp: Double) {
|
fun handleActionMove(ids: IntArray, xs: FloatArray, ys: FloatArray) {
|
||||||
nativeTouchesMove(ids, xs, ys, timestamp)
|
nativeTouchesMove(ids, xs, ys)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleKeyDown(keyCode: Int) {
|
fun handleKeyDown(keyCode: Int) {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.media.AudioDeviceCallback
|
|
||||||
import android.media.AudioDeviceInfo
|
import android.media.AudioDeviceInfo
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -15,45 +14,27 @@ import android.os.Build
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
|
|
||||||
// i never agreed to your licenses! take that
|
// i never agreed to your licenses! take that
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
object FMOD {
|
object FMOD {
|
||||||
private var gContext: Context? = null
|
private var gContext: Context? = null
|
||||||
private var gPluginAudioDeviceCallback: PluginAudioDeviceCallback? = null
|
|
||||||
private val gPluginBroadcastReceiver = PluginBroadcastReceiver()
|
private val gPluginBroadcastReceiver = PluginBroadcastReceiver()
|
||||||
|
|
||||||
external fun OutputAAudioHeadphonesChanged()
|
external fun OutputAAudioHeadphonesChanged()
|
||||||
external fun SetInputEnumerationChanged()
|
|
||||||
external fun SetOutputEnumerationChanged()
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun init(context: Context?) {
|
fun init(context: Context?) {
|
||||||
gContext = context
|
gContext = context
|
||||||
if (context == null) {
|
gContext?.registerReceiver(
|
||||||
return
|
gPluginBroadcastReceiver,
|
||||||
}
|
IntentFilter(Intent.ACTION_HEADSET_PLUG)
|
||||||
|
)
|
||||||
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
|
@JvmStatic
|
||||||
fun close() {
|
fun close() {
|
||||||
val context = gContext
|
val context = gContext
|
||||||
if (context != null) {
|
context?.unregisterReceiver(gPluginBroadcastReceiver)
|
||||||
context.unregisterReceiver(gPluginBroadcastReceiver)
|
|
||||||
(context.getSystemService(Context.AUDIO_SERVICE) as AudioManager)
|
|
||||||
.unregisterAudioDeviceCallback(gPluginAudioDeviceCallback)
|
|
||||||
}
|
|
||||||
gContext = null
|
gContext = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,12 +109,6 @@ 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
|
@JvmStatic
|
||||||
fun fileDescriptorFromUri(str: String?): Int {
|
fun fileDescriptorFromUri(str: String?): Int {
|
||||||
gContext?.apply {
|
gContext?.apply {
|
||||||
@@ -152,82 +127,4 @@ object FMOD {
|
|||||||
OutputAAudioHeadphonesChanged()
|
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) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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="M260,800Q169,800 104.5,737Q40,674 40,583Q40,505 87,444Q134,383 210,366Q235,274 310,217Q385,160 480,160Q597,160 678.5,241.5Q760,323 760,440L760,440L760,440Q829,448 874.5,499.5Q920,551 920,620Q920,695 867.5,747.5Q815,800 740,800L260,800ZM260,720L740,720Q782,720 811,691Q840,662 840,620Q840,578 811,549Q782,520 740,520L680,520L680,440Q680,357 621.5,298.5Q563,240 480,240Q397,240 338.5,298.5Q280,357 280,440L280,440L260,440Q202,440 161,481Q120,522 120,580Q120,638 161,679Q202,720 260,720ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM480,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,500L520,500L520,320L440,320L440,500Z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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="M260,800Q169,800 104.5,737Q40,674 40,583Q40,505 87,444Q134,383 210,366Q235,274 310,217Q385,160 480,160Q597,160 678.5,241.5Q760,323 760,440L760,440L760,440Q829,448 874.5,499.5Q920,551 920,620Q920,695 867.5,747.5Q815,800 740,800L520,800Q487,800 463.5,776.5Q440,753 440,720L440,514L376,576L320,520L480,360L640,520L584,576L520,514L520,720Q520,720 520,720Q520,720 520,720L740,720Q782,720 811,691Q840,662 840,620Q840,578 811,549Q782,520 740,520L680,520L680,440Q680,357 621.5,298.5Q563,240 480,240Q397,240 338.5,298.5Q280,357 280,440L260,440Q202,440 161,481Q120,522 120,580Q120,638 161,679Q202,720 260,720L360,720L360,800L260,800ZM480,520L480,520L480,520Q480,520 480,520Q480,520 480,520L480,520L480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520L480,520L480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520L480,520Z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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="M500,840L444,784L586,642L444,500L500,444L642,586L784,444L840,500L698,642L840,784L784,840L642,698L500,840ZM280,840L280,760L360,760L360,840L280,840ZM200,200L120,200Q120,167 143.5,143.5Q167,120 200,120L200,200ZM280,200L280,120L360,120L360,200L280,200ZM440,200L440,120L520,120L520,200L440,200ZM600,200L600,120L680,120L680,200L600,200ZM760,200L760,120Q793,120 816.5,143.5Q840,167 840,200L760,200ZM200,760L200,840Q167,840 143.5,816.5Q120,793 120,760L200,760ZM120,680L120,600L200,600L200,680L120,680ZM120,520L120,440L200,440L200,520L120,520ZM120,360L120,280L200,280L200,360L120,360ZM760,360L760,280L840,280L840,360L760,360Z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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="M240,880L240,710Q201,693 171.5,664.5Q142,636 121.5,600Q101,564 90.5,523Q80,482 80,440Q80,282 192,181Q304,80 480,80Q656,80 768,181Q880,282 880,440Q880,482 869.5,523Q859,564 838.5,600Q818,636 788.5,664.5Q759,693 720,710L720,880L240,880ZM320,800L360,800L360,720L440,720L440,800L520,800L520,720L600,720L600,800L640,800L640,658Q678,649 707.5,628Q737,607 757.5,578Q778,549 789,514Q800,479 800,440Q800,315 711.5,237.5Q623,160 480,160Q337,160 248.5,237.5Q160,315 160,440Q160,479 171,514Q182,549 202.5,578Q223,607 253,628Q283,649 320,658L320,800ZM420,600L540,600L480,480L420,600ZM340,520Q373,520 396.5,496.5Q420,473 420,440Q420,407 396.5,383.5Q373,360 340,360Q307,360 283.5,383.5Q260,407 260,440Q260,473 283.5,496.5Q307,520 340,520ZM620,520Q653,520 676.5,496.5Q700,473 700,440Q700,407 676.5,383.5Q653,360 620,360Q587,360 563.5,383.5Q540,407 540,440Q540,473 563.5,496.5Q587,520 620,520ZM480,800L480,800Q480,800 480,800Q480,800 480,800Q480,800 480,800Q480,800 480,800Q480,800 480,800Q480,800 480,800Q480,800 480,800Q480,800 480,800Q480,800 480,800Q480,800 480,800Q480,800 480,800Q480,800 480,800L480,800L480,800L480,800L480,800L480,800L480,800L480,800L480,800L480,800Z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -62,25 +62,15 @@
|
|||||||
<string name="launcher_notification_update_success">Geode updated!</string>
|
<string name="launcher_notification_update_success">Geode updated!</string>
|
||||||
<string name="launcher_notification_compatibility">Unsupported Geometry Dash version.</string>
|
<string name="launcher_notification_compatibility">Unsupported Geometry Dash version.</string>
|
||||||
<string name="launcher_notification_compatibility_description">An update is required to receive Geode updates.</string>
|
<string name="launcher_notification_compatibility_description">An update is required to receive Geode updates.</string>
|
||||||
<string name="launcher_notification_forwards_compatibility_description">Geode failed to load.</string>
|
|
||||||
<string name="launcher_notification_forwards_compatibility">Unsupported Geometry Dash version.</string>
|
|
||||||
|
|
||||||
<string name="launcher_notification_update_cta">Tap for more information.</string>
|
<string name="launcher_notification_update_cta">Tap for more information.</string>
|
||||||
|
|
||||||
<string name="launcher_self_update_failed_title">Update failed</string>
|
|
||||||
|
|
||||||
<string name="launcher_self_update_failed">Failed to install launcher update: %1$s.</string>
|
|
||||||
<string name="launcher_update_download_failed">Downloading update failed: %1$s</string>
|
|
||||||
<string name="launcher_self_update_progress">Installing update…</string>
|
|
||||||
|
|
||||||
<string name="launcher_starting_game">Starting Geometry Dash…</string>
|
<string name="launcher_starting_game">Starting Geometry Dash…</string>
|
||||||
<string name="launcher_downloading_update">Downloading update…</string>
|
<string name="launcher_downloading_update">Downloading update…</string>
|
||||||
<string name="launcher_downloading_update_details">(%1$s/%2$s)</string>
|
<string name="launcher_downloading_update_details">(%1$s/%2$s)</string>
|
||||||
<string name="launcher_cancelled_manual">Launch paused.</string>
|
<string name="launcher_cancelled_manual">Launch paused.</string>
|
||||||
<string name="launcher_cancelled_navigated_away">Launch paused.</string>
|
<string name="launcher_cancelled_navigated_away">Launch paused.</string>
|
||||||
<string name="launcher_cancelled_crash">Launch paused due to a crash.</string>
|
<string name="launcher_cancelled_crash">Launch paused due to a crash.</string>
|
||||||
<string name="launcher_cancelled_crash_instant">Geode crashed.</string>
|
<string name="launcher_cancelled_error">Could not start Geode.</string>
|
||||||
<string name="launcher_cancelled_error">Geode cannot be used.</string>
|
|
||||||
<string name="launcher_cancelled_outdated">A Geometry Dash update is required.</string>
|
<string name="launcher_cancelled_outdated">A Geometry Dash update is required.</string>
|
||||||
<string name="launcher_cancelled_restart">Restart</string>
|
<string name="launcher_cancelled_restart">Restart</string>
|
||||||
<string name="launcher_cancelled_resume">Resume</string>
|
<string name="launcher_cancelled_resume">Resume</string>
|
||||||
@@ -103,11 +93,6 @@
|
|||||||
<string name="release_fetch_try_later">Please try again later.</string>
|
<string name="release_fetch_try_later">Please try again later.</string>
|
||||||
<string name="release_fetch_manual_check_required">Externally managed library in use, please manually check for updates if you want to overwrite it.</string>
|
<string name="release_fetch_manual_check_required">Externally managed library in use, please manually check for updates if you want to overwrite it.</string>
|
||||||
|
|
||||||
<string name="release_fetch_external_in_use_short">Update failed. External library in use.</string>
|
|
||||||
<string name="release_fetch_no_internet_short">Update failed. Internet not connected.</string>
|
|
||||||
<string name="release_fetch_generic_short">Update failed. Please try again later.</string>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- GeometryDashActivity strings -->
|
<!-- GeometryDashActivity strings -->
|
||||||
<string name="load_failed_link_error">Failed to link libraries</string>
|
<string name="load_failed_link_error">Failed to link libraries</string>
|
||||||
<string name="load_failed_link_error_description">Game libraries could not be found. Try to:</string>
|
<string name="load_failed_link_error_description">Game libraries could not be found. Try to:</string>
|
||||||
@@ -121,7 +106,7 @@
|
|||||||
<string name="load_failed_recommendation_switch_64bit_abi">Install the non 32-bit version of the launcher.</string>
|
<string name="load_failed_recommendation_switch_64bit_abi">Install the non 32-bit version of the launcher.</string>
|
||||||
<string name="load_failed_recommendation_update">Verify that the latest versions of the launcher and Geometry Dash are installed.</string>
|
<string name="load_failed_recommendation_update">Verify that the latest versions of the launcher and Geometry Dash are installed.</string>
|
||||||
<string name="load_failed_recommendation_report">Report this issue to the developers.</string>
|
<string name="load_failed_recommendation_report">Report this issue to the developers.</string>
|
||||||
<string name="load_failed_recommendation_safe_mode">Enable the safe mode button in Settings.</string>
|
<string name="load_failed_recommendation_safe_mode">Hold the Launch button to launch in safe mode.</string>
|
||||||
<string name="load_failed_recommendation_dev_stl">Devs: If this is a locally built Geode version, please use the c++_shared ANDROID_STL!</string>
|
<string name="load_failed_recommendation_dev_stl">Devs: If this is a locally built Geode version, please use the c++_shared ANDROID_STL!</string>
|
||||||
|
|
||||||
<string name="message_box_cancel">Cancel</string>
|
<string name="message_box_cancel">Cancel</string>
|
||||||
@@ -156,15 +141,6 @@
|
|||||||
<string name="preference_set_application_icon">Application icon</string>
|
<string name="preference_set_application_icon">Application icon</string>
|
||||||
<string name="preference_force_hrr">Force maximum refresh rate</string>
|
<string name="preference_force_hrr">Force maximum refresh rate</string>
|
||||||
<string name="preference_enable_redesign">✨ Enable UI refresh</string>
|
<string name="preference_enable_redesign">✨ Enable UI refresh</string>
|
||||||
<string name="preference_disable_redesign">Disable UI refresh</string>
|
|
||||||
|
|
||||||
<string name="preference_launch_delay">Launch delay</string>
|
|
||||||
<string name="preference_launch_delay_select">Select launch delay</string>
|
|
||||||
<string name="preference_launch_delay_instant">None</string>
|
|
||||||
<plurals name="preference_launch_delay_value">
|
|
||||||
<item quantity="one">%1$d second</item>
|
|
||||||
<item quantity="other">%1$d seconds</item>
|
|
||||||
</plurals>
|
|
||||||
|
|
||||||
<string name="preference_display_mode_name">Display mode</string>
|
<string name="preference_display_mode_name">Display mode</string>
|
||||||
<string name="preference_display_mode_select">Select display mode</string>
|
<string name="preference_display_mode_select">Select display mode</string>
|
||||||
@@ -215,7 +191,6 @@
|
|||||||
<string name="preference_launcher_version_name">Android Launcher</string>
|
<string name="preference_launcher_version_name">Android Launcher</string>
|
||||||
<string name="preference_launcher_version_description">v%1$s</string>
|
<string name="preference_launcher_version_description">v%1$s</string>
|
||||||
<string name="preference_launcher_version">Android Launcher v%1$s</string>
|
<string name="preference_launcher_version">Android Launcher v%1$s</string>
|
||||||
<string name="preference_enable_developer_again">You are already a developer!</string>
|
|
||||||
|
|
||||||
<string name="preference_loader_version_name">Geode</string>
|
<string name="preference_loader_version_name">Geode</string>
|
||||||
<string name="preference_loader_version_description">%1$s</string>
|
<string name="preference_loader_version_description">%1$s</string>
|
||||||
@@ -233,7 +208,7 @@
|
|||||||
<string name="preference_architecture">%1$s (%2$s)</string>
|
<string name="preference_architecture">%1$s (%2$s)</string>
|
||||||
|
|
||||||
<string name="preference_check_for_updates_button">Check for updates</string>
|
<string name="preference_check_for_updates_button">Check for updates</string>
|
||||||
<string name="preference_check_for_updates_description">Last checked %1$s</string>
|
<string name="preference_check_for_updates_description">Current loader version: %1$s</string>
|
||||||
<string name="preference_check_for_updates_failed">Update failed. See logs for more details.</string>
|
<string name="preference_check_for_updates_failed">Update failed. See logs for more details.</string>
|
||||||
<string name="preference_check_for_updates_none_found">No updates found.</string>
|
<string name="preference_check_for_updates_none_found">No updates found.</string>
|
||||||
<string name="preference_check_for_updates_success">Geode has been updated!</string>
|
<string name="preference_check_for_updates_success">Geode has been updated!</string>
|
||||||
@@ -242,38 +217,10 @@
|
|||||||
<string name="preference_check_for_updates_already_updating">An update is already in progress!</string>
|
<string name="preference_check_for_updates_already_updating">An update is already in progress!</string>
|
||||||
|
|
||||||
<string name="preference_category_developer">Advanced</string>
|
<string name="preference_category_developer">Advanced</string>
|
||||||
|
|
||||||
<string name="preference_launch_safe_mode">Launch in safe mode</string>
|
|
||||||
<string name="preference_launch_safe_mode_confirm_title">Start in safe mode?</string>
|
|
||||||
|
|
||||||
<string name="preference_backup_cloud">Backup save to cloud</string>
|
|
||||||
<string name="preference_backup_confirm_title">Backup save data?</string>
|
|
||||||
<string name="preference_backup_in_progress">Backing up data…</string>
|
|
||||||
<string name="preference_backup_cancelling">Cancelling backup…</string>
|
|
||||||
|
|
||||||
<string name="preference_backup_confirm_description">This will backup your save data using your in-game account. You can then access it on an unmodified version of the game through the account backup menu.\nContinuing will overwrite any save data you currently have on the Geometry Dash servers!</string>
|
|
||||||
|
|
||||||
<string name="preference_backup_save_missing">Failed to find save data.</string>
|
|
||||||
<string name="preference_backup_save_decode_failed">Failed to decode save file.</string>
|
|
||||||
<string name="preference_backup_save_too_large">Save file is too large.</string>
|
|
||||||
<string name="preference_backup_save_details_missing">Failed to find account information.</string>
|
|
||||||
|
|
||||||
<string name="preference_backup_success">Backup success!</string>
|
|
||||||
<string name="preference_backup_cancelled">Backup cancelled.</string>
|
|
||||||
<string name="preference_backup_failed_generic">An unknown error has occurred.</string>
|
|
||||||
<string name="preference_backup_failed_invalid_login">Invalid login information.</string>
|
|
||||||
<string name="preference_backup_failed_network">A network error has occurred. Please try again later.</string>
|
|
||||||
|
|
||||||
<string name="preference_backup_failed_title">Backup failed</string>
|
|
||||||
|
|
||||||
<string name="preference_backup_failed">Backup failed with error code %1$d.</string>
|
|
||||||
|
|
||||||
<string name="preference_launch_arguments_name">Launch arguments</string>
|
<string name="preference_launch_arguments_name">Launch arguments</string>
|
||||||
<string name="preference_launch_arguments_set_title">Set launch arguments</string>
|
<string name="preference_launch_arguments_set_title">Set launch arguments</string>
|
||||||
<string name="preference_text_clear">Clear</string>
|
<string name="preference_text_clear">Clear</string>
|
||||||
|
|
||||||
<string name="preference_disable_update_cache">Disable update cache</string>
|
|
||||||
|
|
||||||
<string name="preference_open_developer_options">Open developer options</string>
|
<string name="preference_open_developer_options">Open developer options</string>
|
||||||
<string name="preference_developer_options_dialog_title">Enable developer options?</string>
|
<string name="preference_developer_options_dialog_title">Enable developer options?</string>
|
||||||
|
|
||||||
@@ -319,8 +266,6 @@
|
|||||||
<string name="title_activity_crash_logs">Crash dumps</string>
|
<string name="title_activity_crash_logs">Crash dumps</string>
|
||||||
<string name="application_crashes_no_dumps">No crash dumps available.</string>
|
<string name="application_crashes_no_dumps">No crash dumps available.</string>
|
||||||
<string name="application_crashes_delete_one">Delete</string>
|
<string name="application_crashes_delete_one">Delete</string>
|
||||||
<string name="application_crashes_clear_indicator">Ignore last crash</string>
|
|
||||||
<string name="application_crashes_create_indicator">Fake crash</string>
|
|
||||||
|
|
||||||
<!-- ApplicationIconActivity strings -->
|
<!-- ApplicationIconActivity strings -->
|
||||||
<string name="title_activity_application_icon">Application icons</string>
|
<string name="title_activity_application_icon">Application icons</string>
|
||||||
@@ -329,6 +274,6 @@
|
|||||||
<string name="application_icon_geode">Geode</string>
|
<string name="application_icon_geode">Geode</string>
|
||||||
<string name="application_icon_pride">Pride</string>
|
<string name="application_icon_pride">Pride</string>
|
||||||
<string name="application_icon_trans">Transgender</string>
|
<string name="application_icon_trans">Transgender</string>
|
||||||
<string name="application_icon_confirm">Change icon to %1$s?</string>
|
|
||||||
<string name="application_icon_confirm_description">Geode Launcher will close itself to change the application icon.</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
+6
-5
@@ -1,13 +1,14 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
val composeBOM by extra("2026.02.00")
|
val composeBOM by extra("2025.09.00")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "9.0.1" apply false
|
id("com.android.application") version "8.13.0" apply false
|
||||||
id("com.android.library") version "9.0.1" apply false
|
id("com.android.library") version "8.13.0" apply false
|
||||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.3.10" apply false
|
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||||
id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false
|
id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" apply false
|
||||||
|
id("org.jetbrains.kotlin.plugin.compose") version "2.2.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Delete>("clean") {
|
tasks.register<Delete>("clean") {
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
#Wed Jan 28 04:12:33 MST 2026
|
#Thu Apr 24 10:54:01 MST 2025
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ done
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||||
|
' "$PWD" ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -205,7 +206,7 @@ fi
|
|||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command:
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# and any embedded shellness will be escaped.
|
# and any embedded shellness will be escaped.
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# treated as '${Hostname}' itself on the command line.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|||||||
Reference in new issue
Block a user