mirror of
https://github.com/geode-sdk/android-launcher.git
synced 2026-10-24 00:59:21 +00:00
Compare commits
3
Commits
1.7.0
..
web-portal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
208819436a | ||
|
|
d7c9e82fc2 | ||
|
|
8448b3f2a9 |
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
-10
@@ -10,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")
|
||||||
}
|
}
|
||||||
@@ -20,9 +21,9 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.geode.launcher"
|
applicationId = "com.geode.launcher"
|
||||||
minSdk = 23
|
minSdk = 23
|
||||||
targetSdk = 36
|
targetSdk = 35
|
||||||
versionCode = 23
|
versionCode = 22
|
||||||
versionName = "1.7.0"
|
versionName = "1.6.1"
|
||||||
|
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
@@ -31,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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,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
|
||||||
@@ -96,18 +102,25 @@ dependencies {
|
|||||||
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.10.0")
|
||||||
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
|
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
|
||||||
implementation ("androidx.activity:activity-compose:1.12.3")
|
implementation ("androidx.activity:activity-compose:1.12.1")
|
||||||
implementation ("androidx.activity:activity-ktx:1.12.3")
|
implementation ("androidx.activity:activity-ktx:1.12.1")
|
||||||
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.4")
|
implementation ("com.squareup.okio:okio:3.16.4")
|
||||||
implementation ("com.squareup.okhttp3:okhttp:5.3.2")
|
implementation ("com.squareup.okhttp3:okhttp:5.3.2")
|
||||||
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0")
|
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||||
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.10.0")
|
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.9.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.39.1")
|
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.38.1")
|
||||||
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.39.1")
|
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.38.1")
|
||||||
implementation ("androidx.browser:browser:1.9.0")
|
implementation ("androidx.browser:browser:1.9.0")
|
||||||
|
|
||||||
|
implementation ("io.ktor:ktor-server-core-jvm:3.3.3")
|
||||||
|
implementation ("io.ktor:ktor-server-cio:3.3.3")
|
||||||
|
implementation ("io.ktor:ktor-utils-jvm:3.3.3")
|
||||||
|
implementation ("io.ktor:ktor-server-status-pages:3.3.3")
|
||||||
|
implementation ("io.ktor:ktor-server-sse:3.3.3")
|
||||||
|
|
||||||
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")
|
||||||
}
|
}
|
||||||
Vendored
+1
@@ -5,3 +5,4 @@
|
|||||||
|
|
||||||
# note: if you're going to add more rules, consider the @Keep annotation
|
# note: if you're going to add more rules, consider the @Keep annotation
|
||||||
# this should really only be kept to non-custom code
|
# this should really only be kept to non-custom code
|
||||||
|
-dontwarn java.lang.management.**
|
||||||
@@ -21,9 +21,9 @@
|
|||||||
<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.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.wifi"
|
android:name="android.hardware.wifi"
|
||||||
@@ -31,65 +31,38 @@
|
|||||||
<uses-feature
|
<uses-feature
|
||||||
android:glEsVersion="0x00020000"
|
android:glEsVersion="0x00020000"
|
||||||
android:required="true" />
|
android:required="true" />
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.gamepad"
|
|
||||||
android:required="false" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
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 +70,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 +92,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
|
||||||
@@ -222,6 +158,12 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.game_mode_config"
|
android:name="android.game_mode_config"
|
||||||
android:resource="@xml/game_mode_config" />
|
android:resource="@xml/game_mode_config" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".DeveloperServerService"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
android:exported="false">
|
||||||
|
</service>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Geode Android Web Interface</title>
|
||||||
|
<meta name="color-scheme" content="light dark">
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Quicksand:wght@300..700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
.logo {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: "Quicksand", sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-main {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-name {
|
||||||
|
font-family: "Quicksand", sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logs-area {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
height: 15rem;
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1 class="title">
|
||||||
|
<svg
|
||||||
|
width="698"
|
||||||
|
height="698"
|
||||||
|
viewBox="0 0 696 698"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="logo"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M393.076 24.4223L372.884 9.00248C358.192 -2.21718 337.808 -2.2172 323.116 9.00246L16.9433 242.812C3.28395 253.243 -2.41995 271.086 2.656 287.506L120.413 668.435C125.723 685.612 141.605 697.326 159.584 697.326H536.416C554.395 697.326 570.277 685.612 575.587 668.435L693.344 287.506C698.42 271.086 692.716 253.243 679.057 242.812L650.835 221.26L618.374 323.385L529.198 613.394C525.713 624.725 515.245 632.458 503.39 632.458H190.61C178.755 632.458 168.287 624.725 164.802 613.394L67.1963 295.971C63.8815 285.191 67.6162 273.487 76.562 266.619L330.558 71.6226C331.579 70.8386 332.643 70.1371 333.739 69.5183L393.076 24.4223Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M336.614 143.264C344.511 137.209 355.489 137.209 363.386 143.264L552.797 288.49C560.096 294.086 563.143 303.632 560.437 312.422L487.62 548.924C484.778 558.153 476.251 564.451 466.594 564.451H233.406C223.749 564.451 215.222 558.153 212.38 548.924L199.934 508.5H261.676C262.393 508.647 263.134 508.724 263.888 508.724H432.112C436.963 508.724 441.241 505.546 442.643 500.903L495.141 326.982C496.452 322.638 494.961 317.934 491.386 315.139L354.777 208.3C350.795 205.186 345.205 205.186 341.223 208.3L215.925 306.292L152.151 353.307L139.563 312.422C136.857 303.632 139.904 294.086 147.203 288.49L336.614 143.264Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
Geode Android Manager
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="listing-main">
|
||||||
|
<section>
|
||||||
|
<label for="path-listing">
|
||||||
|
Directory Listing:
|
||||||
|
</label> <br />
|
||||||
|
<select id="path-listing" size=25>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<label for="path-name">Path:</label>
|
||||||
|
<input type="text" id="path-name" />
|
||||||
|
<button id="traverse-btn">Go</button>
|
||||||
|
|
||||||
|
<!--br />
|
||||||
|
|
||||||
|
<input type="file" id="upload-file">
|
||||||
|
<button id="upload-btn">Upload File</button-->
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2><span id="file-name"></span></h2>
|
||||||
|
|
||||||
|
<div class="hidden" id="file-metadata">
|
||||||
|
<p>
|
||||||
|
Size: <span id="file-size"></span> <br />
|
||||||
|
Modified At: <span id="file-modified"></span> <br />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<!-- button id="file-replace">Replace</button>
|
||||||
|
<button id="file-delete">Delete</button>
|
||||||
|
|
||||||
|
<br /-->
|
||||||
|
|
||||||
|
<a href="" download id="file-download">Download</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Logs</h2>
|
||||||
|
|
||||||
|
<pre id="logs-area"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const basePath = "/";
|
||||||
|
|
||||||
|
/** @type {HTMLSelectElement | null} */
|
||||||
|
const pathListing = document.querySelector("#path-listing");
|
||||||
|
|
||||||
|
/** @type {HTMLInputElement | null} */
|
||||||
|
const pathName = document.querySelector("#path-name");
|
||||||
|
|
||||||
|
/** @type {HTMLButtonElement | null} */
|
||||||
|
const goBtn = document.querySelector("#traverse-btn");
|
||||||
|
|
||||||
|
/** @type {HTMLSpanElement | null} */
|
||||||
|
const fileName = document.querySelector("#file-name");
|
||||||
|
|
||||||
|
/** @type {HTMLSpanElement | null} */
|
||||||
|
const fileSize = document.querySelector("#file-size");
|
||||||
|
|
||||||
|
/** @type {HTMLSpanElement | null} */
|
||||||
|
const fileModified = document.querySelector("#file-modified");
|
||||||
|
|
||||||
|
/** @type {HTMLAnchorElement | null} */
|
||||||
|
const fileDownload = document.querySelector("#file-download");
|
||||||
|
|
||||||
|
/** @type {HTMLDivElement | null} */
|
||||||
|
const fileMetadata = document.querySelector("#file-metadata");
|
||||||
|
|
||||||
|
/** @type {HTMLPreElement | null} */
|
||||||
|
const logsArea = document.querySelector("#logs-area");
|
||||||
|
|
||||||
|
const currentListing = {};
|
||||||
|
|
||||||
|
function formatBytes(bytes, decimals = 2) {
|
||||||
|
if (bytes == 0) return "0 B";
|
||||||
|
|
||||||
|
const base = 1024;
|
||||||
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||||
|
|
||||||
|
const unitIndex = Math.floor(Math.log(bytes) / Math.log(base));
|
||||||
|
|
||||||
|
const formattedValue = (bytes / Math.pow(base, unitIndex)).toFixed(decimals);
|
||||||
|
|
||||||
|
return `${formattedValue} ${units[unitIndex]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchDirectoryListing(path) {
|
||||||
|
goBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetch(`${basePath}files/${path}`);
|
||||||
|
const json = await data.json();
|
||||||
|
|
||||||
|
const selectElements = json.sort((a, b) => {
|
||||||
|
const aIsDir = a.type == "directory";
|
||||||
|
const bIsDir = b.type == "directory";
|
||||||
|
|
||||||
|
if (aIsDir && !bIsDir) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!aIsDir && bIsDir) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.filename.localeCompare(b.filename);
|
||||||
|
}).map((f) => {
|
||||||
|
currentListing[f.filename] = f;
|
||||||
|
|
||||||
|
const optionElement = document.createElement("option");
|
||||||
|
optionElement.value = f.filename;
|
||||||
|
|
||||||
|
if (f.type == "directory") {
|
||||||
|
optionElement.innerText = "📁 " + f.filename;
|
||||||
|
} else {
|
||||||
|
optionElement.innerText = f.filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
return optionElement;
|
||||||
|
});
|
||||||
|
|
||||||
|
const parentElement = document.createElement("option");
|
||||||
|
parentElement.value = "..";
|
||||||
|
parentElement.innerText = "⤴️ ..";
|
||||||
|
|
||||||
|
selectElements.unshift(parentElement);
|
||||||
|
|
||||||
|
pathListing.replaceChildren(...selectElements);
|
||||||
|
|
||||||
|
pathName.value = path;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
goBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFileData() {
|
||||||
|
if (pathListing.value == "..") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentFile = currentListing[pathListing.value];
|
||||||
|
|
||||||
|
fileName.innerText = currentFile.filename;
|
||||||
|
|
||||||
|
if (currentFile.type == "directory") {
|
||||||
|
fileMetadata.classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileMetadata.classList.remove("hidden");
|
||||||
|
|
||||||
|
fileSize.innerText = formatBytes(currentFile.size);
|
||||||
|
const modifiedDate = new Date(currentFile.lastModified);
|
||||||
|
fileModified.innerText = new Intl.DateTimeFormat("default", {
|
||||||
|
timeStyle: "long",
|
||||||
|
dateStyle: "long",
|
||||||
|
}).format(modifiedDate);
|
||||||
|
fileDownload.href = `${basePath}files${updatePath(currentFile.filename)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePath(append) {
|
||||||
|
const currentPath = pathName.value;
|
||||||
|
|
||||||
|
if (append == "..") {
|
||||||
|
if (currentPath == "/") {
|
||||||
|
return "/"
|
||||||
|
} else {
|
||||||
|
const path = currentPath.split("/");
|
||||||
|
path.pop();
|
||||||
|
|
||||||
|
return path.join("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPath.endsWith("/")) {
|
||||||
|
return currentPath + append;
|
||||||
|
} else {
|
||||||
|
return `${currentPath}/${append}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pathListing.addEventListener("dblclick", async (event) => {
|
||||||
|
const clickedOption = event.target.value;
|
||||||
|
|
||||||
|
if (clickedOption == "..") {
|
||||||
|
const newPath = updatePath("..");
|
||||||
|
await fetchDirectoryListing(newPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = currentListing[clickedOption];
|
||||||
|
|
||||||
|
if (selected.type == "directory") {
|
||||||
|
const newPath = updatePath(selected.filename);
|
||||||
|
await fetchDirectoryListing(newPath);
|
||||||
|
} else {
|
||||||
|
fileDownload.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pathListing.addEventListener("change", (event) => {
|
||||||
|
const clickedOption = event.target.value;
|
||||||
|
|
||||||
|
updateFileData();
|
||||||
|
});
|
||||||
|
|
||||||
|
goBtn.addEventListener("click", async () => {
|
||||||
|
const goto = pathName.value;
|
||||||
|
await fetchDirectoryListing(goto);
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatLog(line) {
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"process": {
|
||||||
|
"processId": 31123,
|
||||||
|
"threadId": 31123,
|
||||||
|
"processUid": 10486
|
||||||
|
},
|
||||||
|
"time": "2025-12-17T10:38:19.488696104Z",
|
||||||
|
"logId": 4,
|
||||||
|
"priority": "error",
|
||||||
|
"tag": "AndroidRuntime",
|
||||||
|
"message": "..."
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
|
return `[${line.priority.charAt(0)}] ${line.time} - [${line.tag}] ${line.message}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateLogs() {
|
||||||
|
const data = await fetch(`${basePath}logs`);
|
||||||
|
const json = await data.json();
|
||||||
|
|
||||||
|
const lines = json.map((l) => formatLog(l));
|
||||||
|
const message = lines.join("\n");
|
||||||
|
|
||||||
|
logsArea.innerText = message;
|
||||||
|
logsArea.scrollTop = logsArea.scrollHeight - logsArea.clientHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
const unprocessedLogs = [];
|
||||||
|
|
||||||
|
function onTick() {
|
||||||
|
const batch = unprocessedLogs.splice(0, 100);
|
||||||
|
|
||||||
|
if (batch.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = batch.map((l) => formatLog(JSON.parse(l))).join("\n");
|
||||||
|
|
||||||
|
const isScrolledToBottom = (logsArea.scrollHeight - logsArea.clientHeight) <= logsArea.scrollTop + 1;
|
||||||
|
|
||||||
|
logsArea.innerText += `\n${formatted}`;
|
||||||
|
|
||||||
|
if (isScrolledToBottom) {
|
||||||
|
logsArea.scrollTop = logsArea.scrollHeight - logsArea.clientHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function animationFrame() {
|
||||||
|
onTick();
|
||||||
|
|
||||||
|
requestAnimationFrame(animationFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLogsStream() {
|
||||||
|
const evtSource = new EventSource(`${basePath}logs_live`);
|
||||||
|
|
||||||
|
// we store new logs to process on the next frame, to avoid slowdowns when lots of logs come in at once
|
||||||
|
evtSource.addEventListener("log", (event) => {
|
||||||
|
unprocessedLogs.push(event.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async (event) => {
|
||||||
|
await fetchDirectoryListing("/");
|
||||||
|
|
||||||
|
await updateLogs();
|
||||||
|
|
||||||
|
openLogsStream();
|
||||||
|
|
||||||
|
requestAnimationFrame(animationFrame);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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
|
||||||
@@ -24,7 +24,7 @@ object BaseRobTopActivity {
|
|||||||
|
|
||||||
var isPaused = false
|
var isPaused = false
|
||||||
|
|
||||||
lateinit var me: WeakReference<Activity>
|
var me: WeakReference<Activity> = WeakReference(null)
|
||||||
private var shouldResumeSound = true
|
private var shouldResumeSound = true
|
||||||
|
|
||||||
fun setCurrentActivity(currentActivity: Activity) {
|
fun setCurrentActivity(currentActivity: Activity) {
|
||||||
@@ -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,523 @@
|
|||||||
|
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.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(painterResource(R.drawable.icon_warning), contentDescription = null, modifier)
|
||||||
|
else -> Icon(painterResource(R.drawable.icon_info), contentDescription = null, modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun mapCancelReasonToInfo(cancelReason: LaunchViewModel.LaunchCancelReason): LaunchStatusInfo {
|
||||||
|
return when (cancelReason) {
|
||||||
|
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_cancelled_crash)
|
||||||
|
)
|
||||||
|
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.geode_download_title)
|
||||||
|
)
|
||||||
|
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_cancelled_error),
|
||||||
|
details = stringResource(R.string.game_not_found)
|
||||||
|
)
|
||||||
|
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_cancelled_error),
|
||||||
|
details = stringResource(R.string.launcher_cancelled_outdated)
|
||||||
|
)
|
||||||
|
LaunchViewModel.LaunchCancelReason.AUTOMATIC,
|
||||||
|
LaunchViewModel.LaunchCancelReason.MANUAL-> LaunchStatusInfo(
|
||||||
|
title = null
|
||||||
|
)
|
||||||
|
else -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_cancelled_manual)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun mapLaunchStatusToInfo(state: LaunchViewModel.LaunchUIState, inSafeMode: Boolean = false): LaunchStatusInfo {
|
||||||
|
return when (state) {
|
||||||
|
is LaunchViewModel.LaunchUIState.Initial,
|
||||||
|
is LaunchViewModel.LaunchUIState.Working,
|
||||||
|
is LaunchViewModel.LaunchUIState.Ready -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_starting_game),
|
||||||
|
details = if (inSafeMode) stringResource(R.string.launcher_in_safe_mode) else null
|
||||||
|
)
|
||||||
|
is LaunchViewModel.LaunchUIState.UpdateCheck -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.release_fetch_in_progress)
|
||||||
|
)
|
||||||
|
is LaunchViewModel.LaunchUIState.Updating -> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val downloaded = remember(state.downloaded) {
|
||||||
|
Formatter.formatShortFileSize(context, state.downloaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
val outOf = remember(state.outOf) {
|
||||||
|
state.outOf?.run {
|
||||||
|
Formatter.formatShortFileSize(context, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_downloading_update),
|
||||||
|
details = stringResource(R.string.launcher_downloading_update_details, downloaded, outOf ?: "…"),
|
||||||
|
progress = {
|
||||||
|
val progress = state.downloaded / state.outOf!!.toDouble()
|
||||||
|
progress.toFloat()
|
||||||
|
}.takeIf { state.outOf != null }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is LaunchViewModel.LaunchUIState.Cancelled -> {
|
||||||
|
return mapCancelReasonToInfo(state.reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LaunchCancelledBody(statusInfo: LaunchStatusInfo, icon: @Composable () -> Unit, inProgress: Boolean, modifier: Modifier = Modifier) {
|
||||||
|
if (statusInfo.title == null && statusInfo.details == null && !inProgress) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = modifier.width(300.dp)
|
||||||
|
) {
|
||||||
|
if (statusInfo.title != null) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
|
||||||
|
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
|
||||||
|
icon()
|
||||||
|
Spacer(Modifier.size(8.dp))
|
||||||
|
Text(statusInfo.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inProgress) {
|
||||||
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusInfo.details != null) {
|
||||||
|
Text(
|
||||||
|
statusInfo.details,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LaunchProgressBody(statusInfo: LaunchStatusInfo, modifier: Modifier = Modifier) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
modifier = modifier.width(300.dp)
|
||||||
|
) {
|
||||||
|
if (statusInfo.title != null) {
|
||||||
|
Text(statusInfo.title, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusInfo.progress != null) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
statusInfo.progress,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusInfo.details != null) {
|
||||||
|
Text(statusInfo.details, style = MaterialTheme.typography.bodyMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RetryButtonContents(reason: LaunchViewModel.LaunchCancelReason) {
|
||||||
|
when (reason) {
|
||||||
|
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_resume),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
Text(stringResource(R.string.launcher_cancelled_resume))
|
||||||
|
}
|
||||||
|
LaunchViewModel.LaunchCancelReason.MANUAL,
|
||||||
|
LaunchViewModel.LaunchCancelReason.AUTOMATIC -> {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_play_arrow),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
Text(stringResource(R.string.launcher_launch))
|
||||||
|
}
|
||||||
|
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_refresh),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
Text(stringResource(R.string.launcher_cancelled_retry))
|
||||||
|
}
|
||||||
|
// these shouldn't be allowing retries anyways
|
||||||
|
LaunchViewModel.LaunchCancelReason.GAME_MISSING,
|
||||||
|
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun LaunchProgressCard(
|
||||||
|
uiState: LaunchViewModel.LaunchUIState,
|
||||||
|
crashInfo: LoadFailureInfo?,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
onResume: (safeMode: Boolean) -> Unit,
|
||||||
|
onMore: () -> Unit,
|
||||||
|
extraOptions: @Composable () -> Unit,
|
||||||
|
safeModeEnabled: Boolean,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
var showSafeModeDialog by remember { mutableStateOf(false) }
|
||||||
|
val status = mapLaunchStatusToInfo(uiState, safeModeEnabled)
|
||||||
|
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
if (uiState is LaunchViewModel.LaunchUIState.Cancelled) {
|
||||||
|
LaunchCancelledBody(
|
||||||
|
statusInfo = status,
|
||||||
|
icon = { LaunchCancelledIcon(uiState.reason, Modifier.size(32.dp)) },
|
||||||
|
inProgress = uiState.inProgress
|
||||||
|
)
|
||||||
|
|
||||||
|
if (uiState.reason.isGameInstallIssue()) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val showDownload = remember { GamePackageUtils.showDownloadBadge(context.packageManager) }
|
||||||
|
|
||||||
|
if (showDownload) {
|
||||||
|
GooglePlayBadge(modifier = Modifier.align(Alignment.CenterHorizontally))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiState.reason.allowsRetry()) {
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
) {
|
||||||
|
LongPressButton(onClick = { onResume(false) }, onLongPress = {
|
||||||
|
showSafeModeDialog = true
|
||||||
|
}) {
|
||||||
|
RetryButtonContents(uiState.reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crashInfo != null) {
|
||||||
|
FilledTonalButton(onClick = onMore) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_question_mark),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
Text(stringResource(R.string.launcher_cancelled_help_alt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extraOptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LaunchProgressBody(statusInfo = status, modifier = Modifier.padding(top = 16.dp))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.width(300.dp)) {
|
||||||
|
TextButton(onClick = onCancel) {
|
||||||
|
Text(stringResource(R.string.release_fetch_button_cancel))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1.0f))
|
||||||
|
|
||||||
|
extraOptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showSafeModeDialog) {
|
||||||
|
SafeModeDialog(
|
||||||
|
onDismiss = {
|
||||||
|
showSafeModeDialog = false
|
||||||
|
},
|
||||||
|
onLaunch = {
|
||||||
|
onResume(true)
|
||||||
|
showSafeModeDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExtraOptions(onSettings: () -> Unit) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onSettings,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_settings),
|
||||||
|
contentDescription = stringResource(R.string.launcher_settings_icon_alt)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
package com.geode.launcher
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.os.Binder
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.ServiceCompat
|
||||||
|
import com.geode.launcher.log.LogLine
|
||||||
|
import com.geode.launcher.utils.DownloadUtils
|
||||||
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import okio.Buffer
|
||||||
|
import okio.buffer
|
||||||
|
import okio.source
|
||||||
|
import java.io.EOFException
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.nio.file.DirectoryNotEmptyException
|
||||||
|
import kotlin.collections.plusAssign
|
||||||
|
import kotlin.io.path.deleteExisting
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
import kotlin.io.path.isDirectory
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.Instant
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.http.content.PartData
|
||||||
|
import io.ktor.http.content.forEachPart
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import io.ktor.server.engine.*
|
||||||
|
import io.ktor.server.cio.*
|
||||||
|
import io.ktor.server.http.content.staticFiles
|
||||||
|
import io.ktor.server.plugins.statuspages.StatusPages
|
||||||
|
import io.ktor.server.request.receiveChannel
|
||||||
|
import io.ktor.server.request.receiveMultipart
|
||||||
|
import io.ktor.server.sse.*
|
||||||
|
import io.ktor.sse.ServerSentEvent
|
||||||
|
import io.ktor.util.cio.writeChannel
|
||||||
|
import io.ktor.utils.io.copyAndClose
|
||||||
|
|
||||||
|
enum class FileType {
|
||||||
|
@SerialName("file")
|
||||||
|
FILE,
|
||||||
|
@SerialName("directory")
|
||||||
|
DIRECTORY
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FileMeta(val filename: String, val type: FileType, val size: Long, val lastModified: Instant)
|
||||||
|
|
||||||
|
class DeveloperServerService : Service() {
|
||||||
|
private val binder = LocalBinder()
|
||||||
|
|
||||||
|
private var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
|
||||||
|
|
||||||
|
inner class LocalBinder : Binder() {
|
||||||
|
fun getService(): DeveloperServerService = this@DeveloperServerService
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startForeground() {
|
||||||
|
try {
|
||||||
|
val notification = NotificationCompat.Builder(this, "geode_developer_server")
|
||||||
|
.setSmallIcon(R.drawable.geode_monochrome)
|
||||||
|
.build()
|
||||||
|
ServiceCompat.startForeground(
|
||||||
|
this,
|
||||||
|
100,
|
||||||
|
notification,
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
/*
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||||
|
&& e is ForegroundServiceStartNotAllowedException
|
||||||
|
) {
|
||||||
|
println("idk")
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder {
|
||||||
|
return binder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTimeout(startId: Int, fgsType: Int) {
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
|
||||||
|
server?.stop()
|
||||||
|
server = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// not recursive copy
|
||||||
|
private fun copyIndexToCache() {
|
||||||
|
val folderName = "web_interface"
|
||||||
|
val base = File(application.cacheDir, folderName)
|
||||||
|
|
||||||
|
base.deleteRecursively()
|
||||||
|
base.mkdirs()
|
||||||
|
|
||||||
|
assets.list(folderName)?.forEach {
|
||||||
|
val assetName = "$folderName/$it"
|
||||||
|
val outputPath = File(base, it)
|
||||||
|
assets.open(assetName).use { inputStream ->
|
||||||
|
outputPath.outputStream().use { outputStream ->
|
||||||
|
DownloadUtils.copyFile(inputStream, outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createLogsFlow(): Flow<LogLine> = flow {
|
||||||
|
val timestamp = System.currentTimeMillis()
|
||||||
|
val seconds = timestamp / 1000
|
||||||
|
val ms = timestamp % 1000
|
||||||
|
|
||||||
|
// Time format is 'MM-DD hh:mm:ss.mmm...', 'YYYY-MM-DD hh:mm:ss.mmm...', or 'sssss.mmm...'.
|
||||||
|
val timeString = "$seconds.$ms"
|
||||||
|
|
||||||
|
val logCommand = "logcat -B -T $timeString"
|
||||||
|
|
||||||
|
val logProcess = try {
|
||||||
|
Runtime.getRuntime().exec(logCommand)
|
||||||
|
} catch (ioe: IOException) {
|
||||||
|
ioe.printStackTrace()
|
||||||
|
emit(LogLine.showException(ioe))
|
||||||
|
|
||||||
|
return@flow
|
||||||
|
}
|
||||||
|
|
||||||
|
val logBuffer = logProcess.inputStream.source().buffer()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// this runs until the stream is exhausted
|
||||||
|
while (currentCoroutineContext().isActive) {
|
||||||
|
val line = LogLine.fromBufferedSource(logBuffer)
|
||||||
|
emit(line)
|
||||||
|
|
||||||
|
yield()
|
||||||
|
}
|
||||||
|
} catch (_: EOFException) {
|
||||||
|
// ignore, end of file reached
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
emit(LogLine.showException(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun readLogs(): List<LogLine> {
|
||||||
|
val logLines = mutableListOf<LogLine>()
|
||||||
|
|
||||||
|
val logCommand = "logcat -B -d"
|
||||||
|
val logProcess = try {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Runtime.getRuntime().exec(logCommand)
|
||||||
|
}
|
||||||
|
} catch (ioe: IOException) {
|
||||||
|
ioe.printStackTrace()
|
||||||
|
logLines += LogLine.showException(ioe)
|
||||||
|
|
||||||
|
return logLines
|
||||||
|
}
|
||||||
|
|
||||||
|
// read entire log into a buffer so no logs are added to the buffer during processing
|
||||||
|
val logBuffer = Buffer()
|
||||||
|
logProcess.inputStream.source().buffer().readAll(logBuffer)
|
||||||
|
|
||||||
|
try {
|
||||||
|
coroutineScope {
|
||||||
|
// this runs until the stream is exhausted
|
||||||
|
while (true) {
|
||||||
|
val line = LogLine.fromBufferedSource(logBuffer)
|
||||||
|
logLines += line
|
||||||
|
|
||||||
|
yield()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: EOFException) {
|
||||||
|
// ignore, end of file reached
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
logLines += LogLine.showException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return logLines
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
val staticDir = File(application.cacheDir, "web_interface")
|
||||||
|
val baseDir = LaunchUtils.getBaseDirectory(application, true)
|
||||||
|
|
||||||
|
val logsFlow = createLogsFlow()
|
||||||
|
|
||||||
|
server = embeddedServer(CIO, port = 9043) {
|
||||||
|
routing {
|
||||||
|
install(SSE)
|
||||||
|
|
||||||
|
install(StatusPages) {
|
||||||
|
exception<Throwable> { call, cause ->
|
||||||
|
call.respondText(text = "500: $cause" , status = HttpStatusCode.InternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/logs") {
|
||||||
|
val logs = Json.encodeToString(readLogs())
|
||||||
|
call.respondText(logs, contentType = ContentType.Application.Json)
|
||||||
|
}
|
||||||
|
|
||||||
|
post("/stop") {
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
sse("/logs_live") {
|
||||||
|
heartbeat {
|
||||||
|
period = 5000.milliseconds
|
||||||
|
event = ServerSentEvent(comments = "heartbeat")
|
||||||
|
}
|
||||||
|
logsFlow.collect {
|
||||||
|
val log = Json.encodeToString(it)
|
||||||
|
send(log, event = "log", id = it.logId.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
staticFiles("/", staticDir)
|
||||||
|
|
||||||
|
staticFiles("/files", baseDir, index = null) {
|
||||||
|
fallback { requestedPath, call ->
|
||||||
|
val basePath = baseDir.toPath()
|
||||||
|
val builtPath = basePath.resolve(requestedPath).normalize()
|
||||||
|
if (!builtPath.startsWith(basePath) || !builtPath.exists()) {
|
||||||
|
// block path traversal issues
|
||||||
|
call.response.status(HttpStatusCode.NotFound)
|
||||||
|
} else {
|
||||||
|
val filenames = builtPath.toFile().listFiles()?.map {
|
||||||
|
FileMeta(
|
||||||
|
filename = it.name,
|
||||||
|
type = if (it.isDirectory) FileType.DIRECTORY else FileType.FILE,
|
||||||
|
size = it.length(),
|
||||||
|
lastModified = Instant.fromEpochMilliseconds(it.lastModified())
|
||||||
|
)
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
val data = Json.encodeToString(filenames)
|
||||||
|
|
||||||
|
call.respondText(data, contentType = ContentType.Application.Json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete("/files/{path...}") {
|
||||||
|
val pathSegments = call.parameters.getAll("path")
|
||||||
|
val requestedPath = pathSegments?.joinToString("/") ?: ""
|
||||||
|
|
||||||
|
val basePath = baseDir.toPath()
|
||||||
|
val builtPath = basePath.resolve(requestedPath).normalize()
|
||||||
|
if (!builtPath.startsWith(basePath) || !builtPath.exists()) {
|
||||||
|
call.response.status(HttpStatusCode.NotFound)
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
builtPath.deleteExisting()
|
||||||
|
call.response.status(HttpStatusCode.NoContent)
|
||||||
|
} catch (e: DirectoryNotEmptyException) {
|
||||||
|
val message = Json.encodeToString(buildJsonObject {
|
||||||
|
put("message", "Provided directory $requestedPath was not empty")
|
||||||
|
})
|
||||||
|
|
||||||
|
call.respondText(message, status = HttpStatusCode.Conflict, contentType = ContentType.Application.Json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
put("/files/{path...}") {
|
||||||
|
val pathSegments = call.parameters.getAll("path")
|
||||||
|
val requestedPath = pathSegments?.joinToString("/") ?: ""
|
||||||
|
|
||||||
|
val basePath = baseDir.toPath()
|
||||||
|
val builtPath = basePath.resolve(requestedPath).normalize()
|
||||||
|
if (!builtPath.startsWith(basePath)) {
|
||||||
|
call.response.status(HttpStatusCode.NotFound)
|
||||||
|
} else {
|
||||||
|
if (builtPath.isDirectory()) {
|
||||||
|
val multipart = call.receiveMultipart(formFieldLimit = 1024 * 1024 * 100)
|
||||||
|
multipart.forEachPart { part ->
|
||||||
|
if (part is PartData.FileItem) {
|
||||||
|
val fileName = part.originalFileName ?: "uploaded_file"
|
||||||
|
|
||||||
|
val outputPath = builtPath.resolve(fileName).normalize()
|
||||||
|
if (!outputPath.startsWith(basePath)) {
|
||||||
|
val outputFile = outputPath.toFile()
|
||||||
|
part.provider().copyAndClose(outputFile.writeChannel())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
part.dispose()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val outputFile = builtPath.toFile()
|
||||||
|
outputFile.parentFile?.mkdirs()
|
||||||
|
|
||||||
|
call.receiveChannel().copyAndClose(outputFile.writeChannel())
|
||||||
|
}
|
||||||
|
|
||||||
|
call.response.status(HttpStatusCode.NoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
super.onStartCommand(intent, flags, startId)
|
||||||
|
this.startForeground()
|
||||||
|
|
||||||
|
copyIndexToCache()
|
||||||
|
server?.start(wait = false)
|
||||||
|
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
|
super.onTaskRemoved(rootIntent)
|
||||||
|
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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,34 +185,7 @@ 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)
|
||||||
|
|
||||||
@@ -444,8 +415,8 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
this.mGLSurfaceView = glSurfaceView
|
this.mGLSurfaceView = glSurfaceView
|
||||||
frameLayout.addView(this.mGLSurfaceView)
|
frameLayout.addView(this.mGLSurfaceView)
|
||||||
|
|
||||||
val disableNotification = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.DISABLE_REDESIGN)
|
val showNotification = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.ENABLE_REDESIGN)
|
||||||
if (!disableNotification) {
|
if (showNotification) {
|
||||||
val hasCards = determineDisplayedCards(this)
|
val hasCards = determineDisplayedCards(this)
|
||||||
if (hasCards.isNotEmpty()) {
|
if (hasCards.isNotEmpty()) {
|
||||||
val notificationView = ComposeView(this)
|
val notificationView = ComposeView(this)
|
||||||
@@ -498,7 +469,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (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
|
||||||
@@ -617,8 +588,8 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
mGLSurfaceView?.sendTimestampEvents = true
|
mGLSurfaceView?.sendTimestampEvents = true
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
GeodeUtils.CAPABILITY_RICH_INPUT -> {
|
GeodeUtils.CAPABILITY_INTERNAL_CALLBACKS -> {
|
||||||
mGLSurfaceView?.enableControllerEvents()
|
mGLSurfaceView?.sendInternalTimestampEvents = true
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
|
|||||||
@@ -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,18 @@
|
|||||||
package com.geode.launcher
|
package com.geode.launcher
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
import android.os.Build
|
||||||
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.material3.*
|
||||||
import androidx.compose.material3.FilledTonalButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.SnackbarDuration
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.SnackbarResult
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -54,58 +20,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.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.geode.launcher.main.*
|
import com.geode.launcher.main.*
|
||||||
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.updater.ReleaseViewModel
|
||||||
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.LaunchUtils
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
import com.geode.launcher.utils.PreferenceUtils
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
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 +64,30 @@ class MainActivity : ComponentActivity() {
|
|||||||
LoadFailureInfo(LaunchUtils.LauncherError.CRASHED)
|
LoadFailureInfo(LaunchUtils.LauncherError.CRASHED)
|
||||||
} else { null }
|
} else { null }
|
||||||
|
|
||||||
var launchArguments: LaunchArguments? = null
|
if (PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.DEVELOPER_SERVER)) {
|
||||||
val intentData = intent.data
|
val intent = Intent(this, DeveloperServerService::class.java)
|
||||||
if (intent.action == Intent.ACTION_VIEW && intentData != null) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val isLaunch = intentData.path == "/launch"
|
|
||||||
val safeMode = if (isLaunch) {
|
|
||||||
intentData.getBooleanQueryParameter("safe-mode", false)
|
|
||||||
} else false
|
|
||||||
|
|
||||||
launchArguments = LaunchArguments(
|
val name = getString(R.string.notification_channel_development_server)
|
||||||
autoSafeMode = safeMode,
|
val descriptionText = getString(R.string.notification_channel_development_server_description)
|
||||||
forcePause = !isLaunch,
|
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
||||||
forceLaunch = isLaunch,
|
val mChannel = NotificationChannel("geode_developer_server", name, importance)
|
||||||
)
|
mChannel.description = descriptionText
|
||||||
|
|
||||||
|
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.createNotificationChannel(mChannel)
|
||||||
|
|
||||||
|
this.startForegroundService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val redirectToAlt = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.ENABLE_REDESIGN)
|
||||||
|
if (redirectToAlt) {
|
||||||
|
val launchIntent = Intent(this, AltMainActivity::class.java)
|
||||||
|
launchIntent.putExtras(intent)
|
||||||
|
startActivity(launchIntent)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
@@ -141,628 +97,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
|
|
||||||
}
|
|
||||||
|
|
||||||
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.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.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
|
||||||
|
|
||||||
|
var beginLaunch by remember { mutableStateOf(false) }
|
||||||
|
var launchInSafeMode by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(shouldUpdate) {
|
||||||
|
if (shouldUpdate && !releaseViewModel.hasPerformedCheck && gdInstalled) {
|
||||||
|
releaseViewModel.runReleaseCheck()
|
||||||
|
} else {
|
||||||
|
releaseViewModel.useGlobalCheckState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchStatusInfo(
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
title = stringResource(R.string.launcher_downloading_update),
|
|
||||||
details = stringResource(R.string.launcher_downloading_update_details, downloaded, outOf ?: "…"),
|
// automatically show load failed dialog (but not crash)
|
||||||
progress = {
|
var showErrorInfo by remember { mutableStateOf(
|
||||||
val progress = state.downloaded / state.outOf!!.toDouble()
|
loadFailureInfo != null && loadFailureInfo.title != LaunchUtils.LauncherError.CRASHED
|
||||||
progress.toFloat()
|
)}
|
||||||
}.takeIf { state.outOf != null }
|
|
||||||
)
|
val hasError = loadFailureInfo != null
|
||||||
}
|
LaunchedEffect(hasError, showErrorInfo) {
|
||||||
is LaunchViewModel.LaunchUIState.Cancelled -> {
|
if (!showErrorInfo && loadFailureInfo != null) {
|
||||||
return mapCancelReasonToInfo(state.reason)
|
showFailureSnackbar(context, loadFailureInfo, snackbarHostState) {
|
||||||
|
showErrorInfo = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
if (showErrorInfo && loadFailureInfo != null) {
|
||||||
fun LaunchCancelledBody(statusInfo: LaunchStatusInfo, icon: @Composable () -> Unit, inProgress: Boolean, modifier: Modifier = Modifier) {
|
ErrorInfoSheet(loadFailureInfo, onDismiss = { showErrorInfo = false })
|
||||||
if (statusInfo.title == null && statusInfo.details == null && !inProgress) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||||
|
) { innerPadding ->
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
modifier = Modifier
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
.padding(innerPadding)
|
||||||
modifier = modifier.width(300.dp)
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
if (statusInfo.title != null) {
|
Image(
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
|
painter = painterResource(id = R.drawable.geode_logo),
|
||||||
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
|
contentDescription = stringResource(R.string.launcher_logo_alt),
|
||||||
icon()
|
modifier = Modifier.size(100.dp, 100.dp)
|
||||||
Spacer(Modifier.size(8.dp))
|
)
|
||||||
Text(statusInfo.title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inProgress) {
|
|
||||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusInfo.details != null) {
|
|
||||||
Text(
|
Text(
|
||||||
statusInfo.details,
|
stringResource(R.string.launcher_title),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = launcherTitleStyle,
|
||||||
textAlign = TextAlign.Center
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
LaunchBlockedLabel(stringResource(R.string.game_outdated, versionName))
|
||||||
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())
|
val stopLaunch = releaseViewModel.isInUpdate || hasError
|
||||||
}
|
PlayButton(
|
||||||
|
stopAutomaticLaunch = stopLaunch,
|
||||||
if (statusInfo.details != null) {
|
blockLaunch = releaseViewModel.isInUpdate,
|
||||||
Text(statusInfo.details, style = MaterialTheme.typography.bodyMedium)
|
onPlayGame = { safeMode ->
|
||||||
}
|
launchInSafeMode = safeMode
|
||||||
}
|
beginLaunch = true
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RetryButtonContents(reason: LaunchViewModel.LaunchCancelReason) {
|
|
||||||
when (reason) {
|
|
||||||
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_resume),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
|
||||||
Text(stringResource(R.string.launcher_cancelled_resume))
|
|
||||||
}
|
|
||||||
LaunchViewModel.LaunchCancelReason.MANUAL,
|
|
||||||
LaunchViewModel.LaunchCancelReason.AUTOMATIC -> {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_play_arrow),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
|
||||||
Text(stringResource(R.string.launcher_launch))
|
|
||||||
}
|
|
||||||
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_refresh),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
|
||||||
Text(stringResource(R.string.launcher_cancelled_retry))
|
|
||||||
}
|
|
||||||
// these shouldn't be allowing retries anyways
|
|
||||||
LaunchViewModel.LaunchCancelReason.GAME_MISSING,
|
|
||||||
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
|
||||||
fun LaunchProgressCard(
|
|
||||||
uiState: LaunchViewModel.LaunchUIState,
|
|
||||||
crashInfo: LoadFailureInfo?,
|
|
||||||
onCancel: () -> Unit,
|
|
||||||
onResume: (safeMode: Boolean) -> Unit,
|
|
||||||
onMore: () -> Unit,
|
|
||||||
extraOptions: @Composable () -> Unit,
|
|
||||||
safeModeEnabled: Boolean,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
var showSafeModeDialog by remember { mutableStateOf(false) }
|
|
||||||
val status = mapLaunchStatusToInfo(uiState, safeModeEnabled)
|
|
||||||
|
|
||||||
Box(modifier = modifier) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
|
||||||
if (uiState is LaunchViewModel.LaunchUIState.Cancelled) {
|
|
||||||
LaunchCancelledBody(
|
|
||||||
statusInfo = status,
|
|
||||||
icon = { LaunchCancelledIcon(uiState.reason, Modifier.size(32.dp)) },
|
|
||||||
inProgress = uiState.inProgress
|
|
||||||
)
|
|
||||||
|
|
||||||
if (uiState.reason.isGameInstallIssue()) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val showDownload = remember { GamePackageUtils.showDownloadBadge(context.packageManager) }
|
|
||||||
|
|
||||||
if (showDownload) {
|
|
||||||
GooglePlayBadge(modifier = Modifier.align(Alignment.CenterHorizontally))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uiState.reason.allowsRetry()) {
|
|
||||||
FlowRow(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
||||||
) {
|
|
||||||
LongPressButton(onClick = { onResume(false) }, onLongPress = {
|
|
||||||
showSafeModeDialog = true
|
|
||||||
}) {
|
|
||||||
RetryButtonContents(uiState.reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (crashInfo != null) {
|
|
||||||
FilledTonalButton(onClick = onMore) {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_question_mark),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
|
||||||
Text(stringResource(R.string.launcher_cancelled_help_alt))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extraOptions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LaunchProgressBody(statusInfo = status, modifier = Modifier.padding(top = 16.dp))
|
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.width(300.dp)) {
|
|
||||||
TextButton(onClick = onCancel) {
|
|
||||||
Text(stringResource(R.string.release_fetch_button_cancel))
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.weight(1.0f))
|
|
||||||
|
|
||||||
extraOptions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showSafeModeDialog) {
|
|
||||||
SafeModeDialog(
|
|
||||||
onDismiss = {
|
|
||||||
showSafeModeDialog = false
|
|
||||||
},
|
},
|
||||||
onLaunch = {
|
|
||||||
onResume(true)
|
|
||||||
showSafeModeDialog = false
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
if (gdVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
|
||||||
fun ExtraOptions(onSettings: () -> Unit) {
|
UnsupportedVersionWarning()
|
||||||
IconButton(
|
}
|
||||||
onClick = onSettings,
|
}
|
||||||
|
}
|
||||||
|
gdInstalled -> {
|
||||||
|
Text(
|
||||||
|
stringResource(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 = stringResource(R.string.launcher_download_icon_alt)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
Text(stringResource(R.string.launcher_download))
|
||||||
|
}
|
||||||
|
Spacer(Modifier.size(2.dp))
|
||||||
|
IconButton(onClick = { onSettings(context) }) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_settings),
|
painterResource(R.drawable.icon_settings),
|
||||||
contentDescription = stringResource(R.string.launcher_settings_icon_alt)
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else -> LaunchBlockedLabel(stringResource(R.string.game_not_found))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(name = "animated, light")
|
UpdateCard(
|
||||||
@Preview(name = "animated, dark", uiMode = UI_MODE_NIGHT_YES)
|
releaseViewModel,
|
||||||
@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
|
|
||||||
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
|
||||||
.drawWithContent {
|
|
||||||
drawContent()
|
|
||||||
drawRect(
|
|
||||||
brush = innerBrush,
|
|
||||||
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
|
|
||||||
|
|
||||||
Crossfade(
|
|
||||||
targetState = shouldAnimate,
|
|
||||||
animationSpec = tween(durationMillis = 1000, easing = FastOutSlowInEasing),
|
|
||||||
label="logo fade"
|
|
||||||
) { screen ->
|
|
||||||
when (screen) {
|
|
||||||
true -> AnimatedLogo(modifier = Modifier.size(64.dp, 64.dp))
|
|
||||||
false -> Image(
|
|
||||||
painterResource(if (theme == LIGHT)
|
|
||||||
R.drawable.geode_base_light else R.drawable.geode_base
|
|
||||||
),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(64.dp, 64.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.launcher_title),
|
|
||||||
style = launcherTitleStyle,
|
|
||||||
fontSize = 64.sp,
|
|
||||||
modifier = Modifier
|
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
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
launchViewModel.beginLaunchFlow()
|
|
||||||
}
|
|
||||||
|
|
||||||
val launchUIState by launchViewModel.uiState.collectAsState()
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
var launchInSafeMode by remember {
|
|
||||||
mutableStateOf(launchViewModel.launchArguments?.autoSafeMode == true)
|
|
||||||
}
|
|
||||||
|
|
||||||
var showErrorInfo by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
|
||||||
modifier = Modifier.safeDrawingPadding()
|
|
||||||
) { innerPadding ->
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(innerPadding),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(8.dp)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
|
||||||
GeodeLogo(shouldAnimate = launchUIState.isInProgress())
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
LaunchProgressCard(
|
|
||||||
launchUIState,
|
|
||||||
crashInfo = launchViewModel.currentCrashInfo(),
|
|
||||||
onCancel = {
|
|
||||||
launchInSafeMode = false
|
|
||||||
coroutineScope.launch {
|
|
||||||
launchViewModel.cancelLaunch()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onResume = { safeMode ->
|
|
||||||
launchInSafeMode = safeMode
|
|
||||||
|
|
||||||
launchViewModel.clearCrashInfo()
|
|
||||||
coroutineScope.launch {
|
|
||||||
launchViewModel.beginLaunchFlow(true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onMore = {
|
|
||||||
showErrorInfo = true
|
|
||||||
},
|
|
||||||
extraOptions = {
|
|
||||||
ExtraOptions(
|
|
||||||
onSettings = {
|
|
||||||
coroutineScope.launch {
|
|
||||||
launchViewModel.cancelLaunch(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
onSettings(context)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
safeModeEnabled = launchInSafeMode
|
|
||||||
)
|
|
||||||
|
|
||||||
// only show launcher update in a case where a user won't see it ingame
|
|
||||||
if (launchUIState is LaunchViewModel.LaunchUIState.Cancelled) {
|
|
||||||
val gameVersion = remember { GamePackageUtils.getGameVersionCodeOrNull(context.packageManager) }
|
|
||||||
|
|
||||||
if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
|
|
||||||
UnsupportedVersionWarning()
|
|
||||||
}
|
|
||||||
|
|
||||||
val nextLauncherUpdate by launchViewModel.nextLauncherUpdate.collectAsState()
|
|
||||||
if (nextLauncherUpdate != null) {
|
|
||||||
LauncherUpdateIndicator()
|
|
||||||
}
|
|
||||||
|
|
||||||
GeodeUpdateIndicator(snackbarHostState) {
|
|
||||||
launchViewModel.retryUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,7 +45,6 @@ class CrashViewModel(private val application: Application) : ViewModel() {
|
|||||||
|
|
||||||
private var logJob: Job? = null
|
private var logJob: Job? = null
|
||||||
|
|
||||||
@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()) {
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import kotlin.time.Clock
|
|||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -26,10 +29,29 @@ fun BufferedSource.readCString(): String {
|
|||||||
return buffer.toString()
|
return buffer.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class ProcessInformation(val processId: Int, val threadId: Int, val processUid: Int)
|
data class ProcessInformation(val processId: Int, val threadId: Int, val processUid: Int)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
enum class LogPriority {
|
enum class LogPriority {
|
||||||
UNKNOWN, DEFAULT, VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT;
|
@SerialName("unknown")
|
||||||
|
UNKNOWN,
|
||||||
|
@SerialName("default")
|
||||||
|
DEFAULT,
|
||||||
|
@SerialName("verbose")
|
||||||
|
VERBOSE,
|
||||||
|
@SerialName("debug")
|
||||||
|
DEBUG,
|
||||||
|
@SerialName("info")
|
||||||
|
INFO,
|
||||||
|
@SerialName("warn")
|
||||||
|
WARN,
|
||||||
|
@SerialName("error")
|
||||||
|
ERROR,
|
||||||
|
@SerialName("fatal")
|
||||||
|
FATAL,
|
||||||
|
@SerialName("silent")
|
||||||
|
SILENT;
|
||||||
|
|
||||||
class LogPriorityIterator(
|
class LogPriorityIterator(
|
||||||
start: LogPriority,
|
start: LogPriority,
|
||||||
@@ -116,7 +138,8 @@ enum class LogPriority {
|
|||||||
/**
|
/**
|
||||||
* Represents a log entry from logcat.
|
* Represents a log entry from logcat.
|
||||||
*/
|
*/
|
||||||
data class LogLine @OptIn(ExperimentalTime::class) constructor(
|
@Serializable
|
||||||
|
data class LogLine(
|
||||||
val process: ProcessInformation,
|
val process: ProcessInformation,
|
||||||
val time: Instant,
|
val time: Instant,
|
||||||
val logId: Int,
|
val logId: Int,
|
||||||
@@ -198,11 +221,10 @@ data class LogLine @OptIn(ExperimentalTime::class) constructor(
|
|||||||
time = time,
|
time = time,
|
||||||
logId = lid,
|
logId = lid,
|
||||||
tag = tag,
|
tag = tag,
|
||||||
message = message
|
message = message.trimEnd('\u0000')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
|
||||||
fun showException(exception: Exception) = LogLine(
|
fun showException(exception: Exception) = LogLine(
|
||||||
process = ProcessInformation(0, 0, 0),
|
process = ProcessInformation(0, 0, 0),
|
||||||
time = Clock.System.now(),
|
time = Clock.System.now(),
|
||||||
@@ -213,15 +235,15 @@ data class LogLine @OptIn(ExperimentalTime::class) constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
@Transient
|
||||||
val identifier = time.toJavaInstant()
|
val identifier = time.toJavaInstant()
|
||||||
|
|
||||||
val messageTrimmed by lazy {
|
val messageTrimmed by lazy {
|
||||||
this.message.trim { it <= ' ' }
|
this.message.trim { it <= ' ' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
|
||||||
val formattedTime by lazy { this.time.toLocalDateTime(TimeZone.currentSystemDefault()) }
|
val formattedTime by lazy { this.time.toLocalDateTime(TimeZone.currentSystemDefault()) }
|
||||||
|
|
||||||
val asSimpleString by lazy {
|
val asSimpleString by lazy {
|
||||||
"$formattedTime [${this.priority.toChar()}/${this.tag}]: ${this.messageTrimmed}"
|
"$formattedTime [${this.priority.toChar()}/${this.tag}]: ${this.messageTrimmed}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -33,14 +32,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 +55,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 +207,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 +227,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),
|
||||||
|
|||||||
@@ -4,12 +4,17 @@ 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.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 +24,26 @@ 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.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
|
||||||
|
|
||||||
@@ -103,11 +113,11 @@ fun SafeModeDialog(onDismiss: () -> Unit, onLaunch: () -> Unit) {
|
|||||||
AlertDialog(
|
AlertDialog(
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.icon_shield_alert),
|
painterResource(R.drawable.icon_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,6 +194,99 @@ 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(
|
||||||
|
painterResource(R.drawable.icon_play_arrow),
|
||||||
|
contentDescription = stringResource(R.string.launcher_launch_icon_alt)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
Text(stringResource(R.string.launcher_launch))
|
||||||
|
}
|
||||||
|
Spacer(Modifier.size(2.dp))
|
||||||
|
IconButton(onClick = { onSettings(context) }) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_settings),
|
||||||
|
contentDescription = stringResource(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(
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ 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
|
||||||
|
|
||||||
@@ -125,9 +124,7 @@ fun LaunchNotification() {
|
|||||||
// 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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -90,43 +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>
|
|
||||||
|
|
||||||
init {
|
|
||||||
val preferenceUtils = PreferenceUtils.get(application)
|
|
||||||
val initialCancel = !preferenceUtils.getBoolean(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
|
|
||||||
&& !preferenceUtils.getBoolean(PreferenceUtils.Key.LOAD_AUTOMATICALLY)
|
|
||||||
|
|
||||||
// fixes a single frame of progress being displayed when we're not actually going to do work
|
|
||||||
_uiState = MutableStateFlow<LaunchUIState>(
|
|
||||||
if (initialCancel) LaunchUIState.Cancelled(LaunchCancelReason.AUTOMATIC)
|
|
||||||
else LaunchUIState.Initial
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow<LaunchUIState>(LaunchUIState.Initial)
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
val nextLauncherUpdate = ReleaseManager.get(application).availableLauncherUpdate
|
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
|
||||||
@@ -141,13 +106,6 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,15 +157,8 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
|||||||
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GEODE_NOT_FOUND))
|
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GEODE_NOT_FOUND))
|
||||||
}
|
}
|
||||||
|
|
||||||
// if forcing immediate launch, then act as if it's manually started (skips timers)
|
|
||||||
val forceImmediate = launchArguments?.forceLaunch == true
|
|
||||||
if (forceImmediate) {
|
|
||||||
hasManuallyStarted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val loadAutomatically = PreferenceUtils.get(application).getBoolean(PreferenceUtils.Key.LOAD_AUTOMATICALLY)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,17 +201,6 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
|||||||
preReadyCheck()
|
preReadyCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retryUpdate() {
|
|
||||||
ReleaseManager.get(application).checkForUpdates(true)
|
|
||||||
if (ReleaseManager.get(application).isInUpdate) {
|
|
||||||
restoreUpdateState = _uiState.value
|
|
||||||
viewModelScope.launch {
|
|
||||||
determineUpdateStatus()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun cancelLaunch(isAutomatic: Boolean = false) {
|
suspend 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) {
|
||||||
@@ -270,10 +210,10 @@ 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.emit(LaunchUIState.Cancelled(reason, true))
|
|
||||||
releaseManager.cancelUpdate()
|
releaseManager.cancelUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,24 @@
|
|||||||
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.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.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@@ -35,12 +33,10 @@ 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
|
||||||
@@ -49,28 +45,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 androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import com.geode.launcher.InstallReceiver
|
|
||||||
import com.geode.launcher.R
|
import com.geode.launcher.R
|
||||||
|
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.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
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,187 +84,64 @@ 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")
|
||||||
Active,
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
Inactive,
|
|
||||||
Finished;
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow<LauncherUpdateState>(LauncherUpdateState.Inactive)
|
|
||||||
val uiState = _uiState.asStateFlow()
|
|
||||||
|
|
||||||
suspend fun resetState() {
|
|
||||||
_uiState.emit(LauncherUpdateState.Inactive)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun installPackage(context: Context, file: File) {
|
|
||||||
if (!file.exists()) {
|
|
||||||
_uiState.emit(LauncherUpdateState.Finished)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_uiState.emit(LauncherUpdateState.Began)
|
|
||||||
|
|
||||||
val installer = context.packageManager.packageInstaller
|
|
||||||
|
|
||||||
installer.registerSessionCallback(object : PackageInstaller.SessionCallback() {
|
|
||||||
override fun onActiveChanged(sessionId: Int, active: Boolean) {
|
|
||||||
if (active) {
|
|
||||||
_uiState.tryEmit(LauncherUpdateState.Active)
|
|
||||||
} else {
|
|
||||||
_uiState.tryEmit(LauncherUpdateState.Began)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBadgingChanged(sessionId: Int) {}
|
fun installLauncherUpdate(context: Context) {
|
||||||
|
|
||||||
override fun onCreated(sessionId: Int) {
|
|
||||||
_uiState.tryEmit(LauncherUpdateState.Began)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinished(sessionId: Int, success: Boolean) {
|
|
||||||
_uiState.tryEmit(LauncherUpdateState.Finished)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onProgressChanged(sessionId: Int, progress: Float) {
|
|
||||||
_uiState.tryEmit(LauncherUpdateState.Active)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
file.inputStream().use { apkStream ->
|
|
||||||
val length = file.length()
|
|
||||||
|
|
||||||
val params = PackageInstaller.SessionParams(
|
|
||||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
|
|
||||||
}
|
|
||||||
|
|
||||||
val sessionId = installer.createSession(params)
|
|
||||||
val session = installer.openSession(sessionId)
|
|
||||||
|
|
||||||
session.openWrite("package", 0, length).use { sessionStream ->
|
|
||||||
apkStream.copyTo(sessionStream)
|
|
||||||
session.fsync(sessionStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
val intent = Intent(context, InstallReceiver::class.java)
|
|
||||||
val pi = PendingIntent.getBroadcast(
|
|
||||||
context,
|
|
||||||
0,
|
|
||||||
intent,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
||||||
)
|
|
||||||
|
|
||||||
session.commit(pi.intentSender)
|
|
||||||
session.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
suspend fun installLauncherUpdate(context: Context) {
|
|
||||||
val nextUpdate = ReleaseManager.get(context).availableLauncherUpdate.value
|
val nextUpdate = ReleaseManager.get(context).availableLauncherUpdate.value
|
||||||
val launcherDownload = nextUpdate?.getDownload()
|
val launcherDownload = nextUpdate?.getDownload()
|
||||||
|
|
||||||
if (launcherDownload != null) {
|
if (launcherDownload != null) {
|
||||||
val outputFile = launcherDownload.filename
|
val outputFile = launcherDownload.filename
|
||||||
val baseDirectory = context.filesDir
|
val baseDirectory = LaunchUtils.getBaseDirectory(context)
|
||||||
|
|
||||||
val outputPathFile = File(baseDirectory, outputFile)
|
val outputPathFile = File(baseDirectory, outputFile)
|
||||||
if (!outputPathFile.exists()) {
|
if (!outputPathFile.exists()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val outputPath = "${UserDirectoryProvider.ROOT}${outputFile}"
|
||||||
|
|
||||||
|
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, outputPathFile)
|
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
|
||||||
|
val downloadIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
data = Uri.parse(downloadUrl)
|
||||||
|
}
|
||||||
|
|
||||||
LauncherUpdater.resetState()
|
context.startActivity(downloadIntent)
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
Toast.makeText(context, context.getString(R.string.no_activity_found), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, context.getString(R.string.release_fetch_no_releases), Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, context.getString(R.string.release_fetch_no_releases), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class)
|
||||||
fun LauncherUpdateDialog(onDismiss: () -> Unit) {
|
|
||||||
val updateState by LauncherUpdater.uiState.collectAsState()
|
|
||||||
|
|
||||||
val cancellable = updateState != LauncherUpdater.LauncherUpdateState.Active
|
|
||||||
|
|
||||||
LaunchedEffect(updateState) {
|
|
||||||
if (updateState == LauncherUpdater.LauncherUpdateState.Finished) {
|
|
||||||
onDismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancellable) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Dialog(
|
|
||||||
onDismissRequest = {
|
|
||||||
if (cancellable) onDismiss()
|
|
||||||
},
|
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(18.dp),
|
|
||||||
modifier = Modifier.padding(24.dp)
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.launcher_self_update_progress),
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val nextRelease = remember { ReleaseManager.get(context).availableLauncherUpdate.value }
|
val nextRelease = remember { ReleaseManager.get(context).availableLauncherUpdate.value }
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
var beginUpdate by remember { mutableStateOf(false) }
|
|
||||||
if (beginUpdate) {
|
|
||||||
LauncherUpdateDialog(onDismiss = {
|
|
||||||
beginUpdate = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextRelease != null) {
|
if (nextRelease != null) {
|
||||||
ModalBottomSheet(onDismissRequest = { onDismiss() }, sheetState = sheetState) {
|
ModalBottomSheet(onDismissRequest = { onDismiss() }, sheetState = sheetState) {
|
||||||
@@ -294,12 +165,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) },
|
||||||
coroutineScope.launch {
|
|
||||||
installLauncherUpdate(context)
|
|
||||||
beginUpdate = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
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))
|
||||||
@@ -352,7 +218,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) }
|
||||||
|
|
||||||
@@ -362,13 +227,6 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var beginUpdate by remember { mutableStateOf(false) }
|
|
||||||
if (beginUpdate) {
|
|
||||||
LauncherUpdateDialog {
|
|
||||||
beginUpdate = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ElevatedCard(modifier) {
|
ElevatedCard(modifier) {
|
||||||
Column(
|
Column(
|
||||||
// buttons add enough padding already, lower to compensate
|
// buttons add enough padding already, lower to compensate
|
||||||
@@ -393,12 +251,7 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = { installLauncherUpdate(context) },
|
||||||
coroutineScope.launch {
|
|
||||||
installLauncherUpdate(context)
|
|
||||||
beginUpdate = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.launcher_install))
|
Text(stringResource(R.string.launcher_install))
|
||||||
}
|
}
|
||||||
@@ -406,3 +259,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(
|
||||||
|
painterResource(R.drawable.icon_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,8 @@ 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.material3.AlertDialog
|
|
||||||
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 +29,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
|
||||||
@@ -87,40 +83,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 +107,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 +117,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 +151,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 = {
|
||||||
@@ -225,8 +181,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
|
||||||
@@ -45,13 +45,14 @@ 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.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 +146,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 +155,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
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -51,6 +51,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
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ 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.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,10 +38,10 @@ 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
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.geode.launcher.R
|
import com.geode.launcher.R
|
||||||
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
|
||||||
@@ -191,6 +193,18 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
|||||||
description = LaunchUtils.getBaseDirectory(context).path,
|
description = LaunchUtils.getBaseDirectory(context).path,
|
||||||
onClick = { onOpenFolder(context) }
|
onClick = { onOpenFolder(context) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SettingsCard(
|
||||||
|
title = stringResource(R.string.preference_development_server_name),
|
||||||
|
description = stringResource(R.string.preference_development_server_description),
|
||||||
|
preferenceKey = PreferenceUtils.Key.DEVELOPER_SERVER,
|
||||||
|
) { enabled ->
|
||||||
|
if (enabled) {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionsGroup(title = stringResource(R.string.preference_category_updater)) {
|
OptionsGroup(title = stringResource(R.string.preference_category_updater)) {
|
||||||
@@ -207,24 +221,10 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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)) {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -42,10 +41,8 @@ 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.updater.ReleaseViewModel
|
||||||
@@ -61,7 +58,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?) {
|
||||||
@@ -111,7 +107,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
|
||||||
@@ -241,10 +237,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
|
||||||
|
val resources = LocalResources.current
|
||||||
|
|
||||||
|
val currentRelease by PreferenceUtils.useStringPreference(PreferenceUtils.Key.CURRENT_VERSION_TAG)
|
||||||
|
val updateStatus by releaseViewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
var showUpdateInProgress by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showUpdateInProgress) {
|
||||||
|
LaunchedEffect(snackbarHostState) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
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)
|
||||||
|
) {
|
||||||
OptionsGroup(stringResource(R.string.preference_category_testing)) {
|
OptionsGroup(stringResource(R.string.preference_category_testing)) {
|
||||||
SettingsSelectCard(
|
SettingsSelectCard(
|
||||||
title = stringResource(R.string.preference_theme_name),
|
title = stringResource(R.string.preference_theme_name),
|
||||||
@@ -299,11 +358,6 @@ 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(
|
||||||
@@ -375,51 +429,30 @@ fun GameplaySettingsGroup() {
|
|||||||
maxFrameRate = maxFrameRate
|
maxFrameRate = maxFrameRate
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun UpdateSettingsGroup(releaseViewModel: ReleaseViewModel, snackbarHostState: SnackbarHostState, onUpdate: () -> Unit) {
|
|
||||||
val updateStatus by releaseViewModel.uiState.collectAsState()
|
|
||||||
|
|
||||||
OptionsGroup(stringResource(R.string.preference_category_updater)) {
|
OptionsGroup(stringResource(R.string.preference_category_updater)) {
|
||||||
SettingsCard(
|
SettingsCard(
|
||||||
title = stringResource(R.string.preference_update_automatically_name),
|
title = stringResource(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
|
||||||
)
|
)
|
||||||
@@ -427,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) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
OptionsGroup(stringResource(R.string.preference_category_developer)) {
|
OptionsGroup(stringResource(R.string.preference_category_developer)) {
|
||||||
if (developerModeEnabled) {
|
if (developerModeEnabled) {
|
||||||
@@ -460,41 +490,7 @@ fun AdvancedSettingsGroup(developerModeEnabled: Boolean) {
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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) }
|
||||||
@@ -507,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 = {
|
||||||
@@ -566,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)
|
|
||||||
AboutSettingsGroup(developerModeEnabled)
|
|
||||||
|
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ 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.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -757,8 +756,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)
|
||||||
}
|
}
|
||||||
@@ -926,7 +925,7 @@ fun OptionsLabel(title: String, description: String? = null, icon: (@Composable
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsCard(title: String, description: String? = null, icon: (@Composable () -> Unit)? = null, asCard: Boolean = true, preferenceKey: PreferenceUtils.Key) {
|
fun SettingsCard(title: String, description: String? = null, icon: (@Composable () -> Unit)? = null, asCard: Boolean = true, preferenceKey: PreferenceUtils.Key, onChange: ((Boolean) -> Unit)? = null) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val settingEnabled = remember {
|
val settingEnabled = remember {
|
||||||
mutableStateOf(getSetting(context, preferenceKey))
|
mutableStateOf(getSetting(context, preferenceKey))
|
||||||
@@ -942,7 +941,10 @@ fun SettingsCard(title: String, description: String? = null, icon: (@Composable
|
|||||||
},
|
},
|
||||||
modifier = Modifier.toggleable(
|
modifier = Modifier.toggleable(
|
||||||
value = settingEnabled.value,
|
value = settingEnabled.value,
|
||||||
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceKey) },
|
onValueChange = {
|
||||||
|
settingEnabled.value = toggleSetting(context, preferenceKey)
|
||||||
|
onChange?.invoke(settingEnabled.value)
|
||||||
|
},
|
||||||
role = Role.Switch,
|
role = Role.Switch,
|
||||||
),
|
),
|
||||||
backgroundColor = if (asCard)
|
backgroundColor = if (asCard)
|
||||||
@@ -958,7 +960,7 @@ fun SettingsCard(title: String, description: String? = null, icon: (@Composable
|
|||||||
|
|
||||||
@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()
|
||||||
}
|
}
|
||||||
@@ -1011,31 +1013,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))
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ import okio.buffer
|
|||||||
import okio.source
|
import okio.source
|
||||||
import java.io.File
|
import java.io.File
|
||||||
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"
|
||||||
@@ -136,7 +134,7 @@ class ReleaseManager private constructor(
|
|||||||
private suspend fun downloadLauncherUpdate(release: Downloadable) {
|
private suspend fun downloadLauncherUpdate(release: Downloadable) {
|
||||||
val download = release.getDownload() ?: return
|
val download = release.getDownload() ?: return
|
||||||
|
|
||||||
val outputDirectory = applicationContext.filesDir
|
val outputDirectory = LaunchUtils.getBaseDirectory(applicationContext)
|
||||||
val outputFile = File(outputDirectory, download.filename)
|
val outputFile = File(outputDirectory, download.filename)
|
||||||
|
|
||||||
if (outputFile.exists()) {
|
if (outputFile.exists()) {
|
||||||
@@ -247,16 +245,6 @@ class ReleaseManager private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkForNewRelease(isManual: Boolean = false) {
|
private suspend fun checkForNewRelease(isManual: Boolean = false) {
|
||||||
val sharedPreferences = PreferenceUtils.get(applicationContext)
|
|
||||||
val lastCheckTime = sharedPreferences.getLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME)
|
|
||||||
|
|
||||||
// only check for updates if it's been over 15 minutes since last check
|
|
||||||
val checkMinTime = Clock.System.now().minus(15.minutes).toEpochMilliseconds()
|
|
||||||
if (!isManual && lastCheckTime > checkMinTime && !fileWasExternallyModified()) {
|
|
||||||
_uiState.value = ReleaseManagerState.Finished()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val release = try {
|
val release = try {
|
||||||
getLatestRelease()
|
getLatestRelease()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -275,9 +263,6 @@ class ReleaseManager private constructor(
|
|||||||
checkLauncherUpdate(launcherRelease)
|
checkLauncherUpdate(launcherRelease)
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentTime = Clock.System.now().toEpochMilliseconds()
|
|
||||||
sharedPreferences.setLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME, currentTime)
|
|
||||||
|
|
||||||
if (release == null) {
|
if (release == null) {
|
||||||
downloadLauncherUpdateIfNecessary()
|
downloadLauncherUpdateIfNecessary()
|
||||||
|
|
||||||
@@ -285,6 +270,8 @@ class ReleaseManager private constructor(
|
|||||||
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()
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ import android.content.ClipboardManager
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.hardware.lights.Light
|
|
||||||
import android.hardware.lights.LightState
|
|
||||||
import android.hardware.lights.LightsRequest
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
@@ -19,7 +16,6 @@ import android.os.VibratorManager
|
|||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
|
import android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.InputDevice
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -550,7 +546,7 @@ object GeodeUtils {
|
|||||||
|
|
||||||
const val CAPABILITY_EXTENDED_INPUT = "extended_input"
|
const val CAPABILITY_EXTENDED_INPUT = "extended_input"
|
||||||
const val CAPABILITY_TIMESTAMP_INPUT = "timestamp_inputs"
|
const val CAPABILITY_TIMESTAMP_INPUT = "timestamp_inputs"
|
||||||
const val CAPABILITY_RICH_INPUT = "internal_callbacks_v2"
|
const val CAPABILITY_INTERNAL_CALLBACKS = "internal_callbacks_v1"
|
||||||
|
|
||||||
private var capabilityListener: WeakReference<CapabilityListener?> = WeakReference(null)
|
private var capabilityListener: WeakReference<CapabilityListener?> = WeakReference(null)
|
||||||
|
|
||||||
@@ -580,188 +576,6 @@ object GeodeUtils {
|
|||||||
.launchUrl(activity, url.toUri())
|
.launchUrl(activity, url.toUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if any gamepad or joystick input devices are connected.
|
|
||||||
* This is useful during start, but tracking inputDeviceAdded events is better during runtime.
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun controllersConnected(): Int {
|
|
||||||
val deviceIds = InputDevice.getDeviceIds()
|
|
||||||
return deviceIds.count {
|
|
||||||
InputDevice.getDevice(it)?.run {
|
|
||||||
sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD || sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK
|
|
||||||
} ?: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getConnectedDevices(): IntArray = InputDevice.getDeviceIds()
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getDevice(deviceId: Int): InputDevice? = InputDevice.getDevice(deviceId)
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getDeviceBatteryCapacity(deviceId: Int): Float {
|
|
||||||
val device = InputDevice.getDevice(deviceId) ?: return 0.0f
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
device.batteryState.capacity
|
|
||||||
} else {
|
|
||||||
0.0f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun deviceHasBattery(deviceId: Int): Boolean {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val device = InputDevice.getDevice(deviceId) ?: return false
|
|
||||||
return device.batteryState.isPresent
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getDeviceBatteryStatus(deviceId: Int): Int {
|
|
||||||
val device = InputDevice.getDevice(deviceId) ?: return 0
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
device.batteryState.status
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getDeviceLightsCount(deviceId: Int): Int {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
val device = InputDevice.getDevice(deviceId) ?: return 0
|
|
||||||
val lightsManager = device.lightsManager
|
|
||||||
|
|
||||||
return lightsManager.lights.size
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is based on paddleboat's api
|
|
||||||
@JvmStatic
|
|
||||||
fun getLightType(deviceId: Int): Int {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
val device = InputDevice.getDevice(deviceId) ?: return 0
|
|
||||||
val lightsManager = device.lightsManager
|
|
||||||
|
|
||||||
var flags = 0
|
|
||||||
lightsManager.lights.forEach { light ->
|
|
||||||
if (light.type == Light.LIGHT_TYPE_PLAYER_ID) {
|
|
||||||
flags = flags or 1
|
|
||||||
} else if (light.hasRgbControl()) {
|
|
||||||
flags = flags or 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return flags
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the color for a device.
|
|
||||||
* @return false if the given device does not exist or setting lights was otherwise unsuccessful
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun setDeviceLightColor(deviceId: Int, color: Int, type: Int): Boolean {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val device = InputDevice.getDevice(deviceId) ?: return false
|
|
||||||
val lightsManager = device.lightsManager
|
|
||||||
|
|
||||||
val lights = lightsManager.lights
|
|
||||||
if (lights.isEmpty()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val actedLights = lights.count { light ->
|
|
||||||
val state = LightState.Builder()
|
|
||||||
|
|
||||||
if (light.type == Light.LIGHT_TYPE_PLAYER_ID && (type and 1 == 1)) {
|
|
||||||
state.setPlayerId(color)
|
|
||||||
} else if (light.hasRgbControl() && (type and 2 == 2)) {
|
|
||||||
state.setColor(color)
|
|
||||||
} else {
|
|
||||||
return@count false
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = LightsRequest.Builder()
|
|
||||||
.addLight(light, state.build())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
lightsManager.openSession()
|
|
||||||
.requestLights(request)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
return actedLights > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getDeviceHapticsCount(deviceId: Int): Int {
|
|
||||||
val device = InputDevice.getDevice(deviceId) ?: return 0
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
|
||||||
return if (device.vibrator.hasVibrator()) 1 else 0
|
|
||||||
}
|
|
||||||
|
|
||||||
val vibratorManager = device.vibratorManager
|
|
||||||
val vibratorIds = vibratorManager.vibratorIds
|
|
||||||
return vibratorIds.size
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun performVibration(vibrator: Vibrator?, durationMs: Long, intensity: Int) {
|
|
||||||
vibrator?.apply {
|
|
||||||
if (intensity == 0) {
|
|
||||||
cancel()
|
|
||||||
} else {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val vibrationEffect = VibrationEffect.createOneShot(durationMs, intensity)
|
|
||||||
vibrate(vibrationEffect)
|
|
||||||
} else {
|
|
||||||
vibrate(durationMs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vibrates a given device. Set motorIdx == -1 to vibrate all available motors on a device.
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun vibrateDevice(deviceId: Int, durationMs: Long, intensity: Int, motorIdx: Int): Boolean {
|
|
||||||
val device = InputDevice.getDevice(deviceId) ?: return false
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
val vibratorManager = device.vibratorManager
|
|
||||||
val vibratorIds = vibratorManager.vibratorIds
|
|
||||||
|
|
||||||
if (motorIdx == -1) {
|
|
||||||
vibratorIds.forEach {
|
|
||||||
val vibrator = vibratorManager.getVibrator(it)
|
|
||||||
performVibration(vibrator, durationMs, intensity)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val deviceId = vibratorIds.getOrNull(motorIdx) ?: return false
|
|
||||||
val vibrator = vibratorManager.getVibrator(deviceId)
|
|
||||||
performVibration(vibrator, durationMs, intensity)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val vibrator = device.vibrator
|
|
||||||
performVibration(vibrator, durationMs, intensity)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
external fun nativeKeyUp(keyCode: Int, modifiers: Int)
|
external fun nativeKeyUp(keyCode: Int, modifiers: Int)
|
||||||
external fun nativeKeyDown(keyCode: Int, modifiers: Int, isRepeating: Boolean)
|
external fun nativeKeyDown(keyCode: Int, modifiers: Int, isRepeating: Boolean)
|
||||||
external fun nativeActionScroll(scrollX: Float, scrollY: Float)
|
external fun nativeActionScroll(scrollX: Float, scrollY: Float)
|
||||||
@@ -773,18 +587,5 @@ object GeodeUtils {
|
|||||||
* @see reportPlatformCapability
|
* @see reportPlatformCapability
|
||||||
*/
|
*/
|
||||||
external fun setNextInputTimestamp(timestamp: Long)
|
external fun setNextInputTimestamp(timestamp: Long)
|
||||||
|
external fun setNextInputTimestampInternal(timestamp: Long)
|
||||||
const val TOUCH_TYPE_BEGIN = 0
|
|
||||||
const val TOUCH_TYPE_MOVE = 1
|
|
||||||
const val TOUCH_TYPE_END = 2
|
|
||||||
const val TOUCH_TYPE_CANCEL = 3
|
|
||||||
|
|
||||||
external fun internalKeyEvent(timestamp: Long, deviceId: Int, eventSource: Int, keyCode: Int, modifiers: Int, isDown: Boolean, repeatCount: Int)
|
|
||||||
external fun internalTouchEvent(timestamp: Long, deviceId: Int, eventSource: Int, eventType: Int, ids: IntArray, xs: FloatArray, ys: FloatArray)
|
|
||||||
external fun internalScrollEvent(timestamp: Long, deviceId: Int, eventSource: Int, scrollX: Float, scrollY: Float)
|
|
||||||
external fun internalJoystickEvent(timestamp: Long, deviceId: Int, eventSource: Int, leftX: FloatArray, leftY: FloatArray, rightX: FloatArray, rightY: FloatArray, hatX: FloatArray, hatY: FloatArray, leftTrigger: FloatArray, rightTrigger: FloatArray)
|
|
||||||
|
|
||||||
external fun inputDeviceAdded(deviceId: Int)
|
|
||||||
external fun inputDeviceChanged(deviceId: Int)
|
|
||||||
external fun inputDeviceRemoved(deviceId: Int)
|
|
||||||
}
|
}
|
||||||
@@ -139,6 +139,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
LIMIT_ASPECT_RATIO,
|
LIMIT_ASPECT_RATIO,
|
||||||
DISPLAY_MODE,
|
DISPLAY_MODE,
|
||||||
FORCE_HRR,
|
FORCE_HRR,
|
||||||
|
ENABLE_REDESIGN,
|
||||||
RELEASE_CHANNEL_TAG,
|
RELEASE_CHANNEL_TAG,
|
||||||
DEVELOPER_MODE,
|
DEVELOPER_MODE,
|
||||||
CLEANUP_APKS,
|
CLEANUP_APKS,
|
||||||
@@ -149,9 +150,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
LIMIT_FRAME_RATE,
|
LIMIT_FRAME_RATE,
|
||||||
DISABLE_USER_THEME,
|
DISABLE_USER_THEME,
|
||||||
SELECTED_ICON,
|
SELECTED_ICON,
|
||||||
DISABLE_REDESIGN,
|
DEVELOPER_SERVER,
|
||||||
WAIT_PERIOD,
|
|
||||||
LAST_UPDATE_CHECK_TIME
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
||||||
@@ -165,7 +164,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
|
||||||
}
|
}
|
||||||
@@ -192,7 +190,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
Key.LIMIT_ASPECT_RATIO -> "PreferenceLimitAspectRatio"
|
Key.LIMIT_ASPECT_RATIO -> "PreferenceLimitAspectRatio"
|
||||||
Key.DISPLAY_MODE -> "PreferenceDisplayMode"
|
Key.DISPLAY_MODE -> "PreferenceDisplayMode"
|
||||||
Key.FORCE_HRR -> "PreferenceForceHighRefreshRate"
|
Key.FORCE_HRR -> "PreferenceForceHighRefreshRate"
|
||||||
Key.DISABLE_REDESIGN -> "PreferenceDisableRedesign"
|
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"
|
||||||
@@ -203,8 +201,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
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.DEVELOPER_SERVER -> "PreferenceDeveloperServer"
|
||||||
Key.LAST_UPDATE_CHECK_TIME -> "PreferenceLastUpdateCheckTime"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ package org.cocos2dx.lib
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.hardware.input.InputManager
|
|
||||||
import android.opengl.GLSurfaceView
|
import android.opengl.GLSurfaceView
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.Message
|
import android.os.Message
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.InputDevice
|
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
@@ -18,12 +16,10 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.appcompat.widget.AppCompatEditText
|
import androidx.appcompat.widget.AppCompatEditText
|
||||||
import com.customRobTop.BaseRobTopActivity
|
import com.customRobTop.BaseRobTopActivity
|
||||||
import com.geode.launcher.utils.GeodeUtils
|
import com.geode.launcher.utils.GeodeUtils
|
||||||
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 {
|
||||||
@@ -60,9 +56,8 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var sendTimestampEvents = false
|
var sendTimestampEvents = false
|
||||||
|
var sendInternalTimestampEvents = false
|
||||||
var manualBackEvents = false
|
var manualBackEvents = false
|
||||||
var controllerEventsEnabled = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
var cocos2dxEditText: AppCompatEditText? = null
|
var cocos2dxEditText: AppCompatEditText? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -159,6 +154,9 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
private fun sendNextEventTimestamp(timestamp: Long) {
|
private fun sendNextEventTimestamp(timestamp: Long) {
|
||||||
if (sendTimestampEvents)
|
if (sendTimestampEvents)
|
||||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
GeodeUtils.setNextInputTimestamp(timestamp)
|
||||||
|
|
||||||
|
if (sendInternalTimestampEvents)
|
||||||
|
GeodeUtils.setNextInputTimestampInternal(timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTouchEvent(motionEvent: MotionEvent): Boolean {
|
override fun onTouchEvent(motionEvent: MotionEvent): Boolean {
|
||||||
@@ -168,7 +166,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)
|
||||||
@@ -182,15 +179,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
val f = ys[0]
|
val f = ys[0]
|
||||||
queueEvent {
|
queueEvent {
|
||||||
sendNextEventTimestamp(timestamp)
|
sendNextEventTimestamp(timestamp)
|
||||||
|
cocos2dxRenderer.handleActionDown(idDown, xDown, f)
|
||||||
if (controllerEventsEnabled)
|
|
||||||
GeodeUtils.internalTouchEvent(
|
|
||||||
timestamp, motionEvent.deviceId, motionEvent.source,
|
|
||||||
// slice here to convert to single element array
|
|
||||||
GeodeUtils.TOUCH_TYPE_BEGIN, ids.sliceArray(0..0), xs.sliceArray(0..0), ys.sliceArray(0..0)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
cocos2dxRenderer.handleActionDown(idDown, xDown, f, timestampSec)
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -200,42 +189,21 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
val f3 = ys[0]
|
val f3 = ys[0]
|
||||||
queueEvent {
|
queueEvent {
|
||||||
sendNextEventTimestamp(timestamp)
|
sendNextEventTimestamp(timestamp)
|
||||||
|
cocos2dxRenderer.handleActionUp(idUp, f2, f3)
|
||||||
if (controllerEventsEnabled)
|
|
||||||
GeodeUtils.internalTouchEvent(
|
|
||||||
timestamp, motionEvent.deviceId, motionEvent.source,
|
|
||||||
GeodeUtils.TOUCH_TYPE_END, ids.sliceArray(0..0), xs.sliceArray(0..0), ys.sliceArray(0..0)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
cocos2dxRenderer.handleActionUp(idUp, f2, f3, timestampSec)
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_MOVE -> {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
queueEvent {
|
queueEvent {
|
||||||
sendNextEventTimestamp(timestamp)
|
sendNextEventTimestamp(timestamp)
|
||||||
|
cocos2dxRenderer.handleActionMove(ids, xs, ys)
|
||||||
if (controllerEventsEnabled)
|
|
||||||
GeodeUtils.internalTouchEvent(
|
|
||||||
timestamp, motionEvent.deviceId, motionEvent.source,
|
|
||||||
GeodeUtils.TOUCH_TYPE_MOVE, ids, xs, ys
|
|
||||||
)
|
|
||||||
else
|
|
||||||
cocos2dxRenderer.handleActionMove(ids, xs, ys, timestampSec)
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_CANCEL -> {
|
MotionEvent.ACTION_CANCEL -> {
|
||||||
queueEvent {
|
queueEvent {
|
||||||
sendNextEventTimestamp(timestamp)
|
sendNextEventTimestamp(timestamp)
|
||||||
|
cocos2dxRenderer.handleActionCancel(ids, xs, ys)
|
||||||
if (controllerEventsEnabled)
|
|
||||||
GeodeUtils.internalTouchEvent(
|
|
||||||
timestamp, motionEvent.deviceId, motionEvent.source,
|
|
||||||
GeodeUtils.TOUCH_TYPE_CANCEL, ids, xs, ys
|
|
||||||
)
|
|
||||||
else
|
|
||||||
cocos2dxRenderer.handleActionCancel(ids, xs, ys, timestampSec)
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -246,14 +214,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
val y = motionEvent.getY(indexPointerDown)
|
val y = motionEvent.getY(indexPointerDown)
|
||||||
queueEvent {
|
queueEvent {
|
||||||
sendNextEventTimestamp(timestamp)
|
sendNextEventTimestamp(timestamp)
|
||||||
|
cocos2dxRenderer.handleActionDown(idPointerDown, xPointerDown, y)
|
||||||
if (controllerEventsEnabled)
|
|
||||||
GeodeUtils.internalTouchEvent(
|
|
||||||
timestamp, motionEvent.deviceId, motionEvent.source,
|
|
||||||
GeodeUtils.TOUCH_TYPE_BEGIN, intArrayOf(idPointerDown), floatArrayOf(xPointerDown), floatArrayOf(y)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
cocos2dxRenderer.handleActionDown(idPointerDown, xPointerDown, y, timestampSec)
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -264,14 +225,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
val y2 = motionEvent.getY(indexPointUp)
|
val y2 = motionEvent.getY(indexPointUp)
|
||||||
queueEvent {
|
queueEvent {
|
||||||
sendNextEventTimestamp(timestamp)
|
sendNextEventTimestamp(timestamp)
|
||||||
|
cocos2dxRenderer.handleActionUp(idPointerUp, xPointerUp, y2)
|
||||||
if (controllerEventsEnabled)
|
|
||||||
GeodeUtils.internalTouchEvent(
|
|
||||||
timestamp, motionEvent.deviceId, motionEvent.source,
|
|
||||||
GeodeUtils.TOUCH_TYPE_END, intArrayOf(idPointerUp), floatArrayOf(xPointerUp), floatArrayOf(y2)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
cocos2dxRenderer.handleActionUp(idPointerUp, xPointerUp, y2, timestampSec)
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -312,42 +266,21 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (useKeyboardEvents) {
|
||||||
|
queueEvent {
|
||||||
|
cocos2dxRenderer.handleKeyDown(KeyEvent.KEYCODE_BACK)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
val currentTime = System.nanoTime()
|
val currentTime = System.nanoTime()
|
||||||
queueEvent {
|
queueEvent {
|
||||||
sendNextEventTimestamp(currentTime)
|
sendNextEventTimestamp(currentTime)
|
||||||
|
|
||||||
if (controllerEventsEnabled) {
|
|
||||||
GeodeUtils.internalKeyEvent(
|
|
||||||
currentTime,
|
|
||||||
-1,
|
|
||||||
InputDevice.SOURCE_ANY,
|
|
||||||
KeyEvent.KEYCODE_BACK,
|
|
||||||
0,
|
|
||||||
true,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
GeodeUtils.internalKeyEvent(
|
|
||||||
currentTime,
|
|
||||||
-1,
|
|
||||||
InputDevice.SOURCE_ANY,
|
|
||||||
KeyEvent.KEYCODE_BACK,
|
|
||||||
0,
|
|
||||||
false,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useKeyboardEvents) {
|
|
||||||
GeodeUtils.nativeKeyDown(KeyEvent.KEYCODE_BACK, 0, false)
|
GeodeUtils.nativeKeyDown(KeyEvent.KEYCODE_BACK, 0, false)
|
||||||
GeodeUtils.nativeKeyUp(KeyEvent.KEYCODE_BACK, 0)
|
|
||||||
} else if (!controllerEventsEnabled) {
|
|
||||||
cocos2dxRenderer.handleKeyDown(KeyEvent.KEYCODE_BACK)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
if (!useKeyboardEvents && !controllerEventsEnabled) {
|
if (!useKeyboardEvents) {
|
||||||
return legacyKeyDown(keyCode, event)
|
return legacyKeyDown(keyCode, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,22 +296,15 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
|
|
||||||
queueEvent {
|
queueEvent {
|
||||||
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
|
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
|
||||||
|
|
||||||
if (useKeyboardEvents) {
|
|
||||||
GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0)
|
GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (controllerEventsEnabled) {
|
|
||||||
GeodeUtils.internalKeyEvent(event.eventTime * MS_TO_NS, event.deviceId, event.source, event.keyCode, event.modifiers, true, event.repeatCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
if (!useKeyboardEvents && !controllerEventsEnabled) {
|
if (!useKeyboardEvents) {
|
||||||
return super.onKeyUp(keyCode, event)
|
return super.onKeyUp(keyCode, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,82 +319,16 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
|
|
||||||
queueEvent {
|
queueEvent {
|
||||||
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
|
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
|
||||||
|
|
||||||
if (useKeyboardEvents) {
|
|
||||||
GeodeUtils.nativeKeyUp(keyCode, event.modifiers)
|
GeodeUtils.nativeKeyUp(keyCode, event.modifiers)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (controllerEventsEnabled) {
|
|
||||||
GeodeUtils.internalKeyEvent(event.eventTime * MS_TO_NS, event.deviceId, event.source, event.keyCode, event.modifiers, false, event.repeatCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// i'm lazy.
|
|
||||||
// https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input
|
|
||||||
private fun getCenteredAxis(
|
|
||||||
event: MotionEvent,
|
|
||||||
device: InputDevice,
|
|
||||||
axis: Int,
|
|
||||||
historyPos: Int
|
|
||||||
): Float {
|
|
||||||
device.getMotionRange(axis, event.source)?.apply {
|
|
||||||
val value: Float = if (historyPos < 0) {
|
|
||||||
event.getAxisValue(axis)
|
|
||||||
} else {
|
|
||||||
event.getHistoricalAxisValue(axis, historyPos)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (abs(value) > flat) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0.0f
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getJoystickMotion(event: MotionEvent, axisHorizontal: Int, axisVertical: Int): Pair<FloatArray, FloatArray> {
|
|
||||||
val positionChangesX = ArrayList<Float>()
|
|
||||||
val positionChangesY = ArrayList<Float>()
|
|
||||||
|
|
||||||
val inputDevice = event.device
|
|
||||||
|
|
||||||
(0 until event.historySize).forEach { i ->
|
|
||||||
val x = getCenteredAxis(event, inputDevice, axisHorizontal, i)
|
|
||||||
val y = getCenteredAxis(event, inputDevice, axisVertical, i)
|
|
||||||
positionChangesX.add(x)
|
|
||||||
positionChangesY.add(y)
|
|
||||||
}
|
|
||||||
|
|
||||||
val x = getCenteredAxis(event, inputDevice, axisHorizontal, -1)
|
|
||||||
val y = getCenteredAxis(event, inputDevice, axisVertical, -1)
|
|
||||||
positionChangesX.add(x)
|
|
||||||
positionChangesY.add(y)
|
|
||||||
|
|
||||||
return Pair(positionChangesX.toFloatArray(), positionChangesY.toFloatArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getJoystickMotion(event: MotionEvent, axis: Int): FloatArray {
|
|
||||||
val positionChanges = ArrayList<Float>()
|
|
||||||
|
|
||||||
val inputDevice = event.device
|
|
||||||
|
|
||||||
(0 until event.historySize).forEach { i ->
|
|
||||||
val x = getCenteredAxis(event, inputDevice, axis, i)
|
|
||||||
positionChanges.add(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
val x = getCenteredAxis(event, inputDevice, axis, -1)
|
|
||||||
positionChanges.add(x)
|
|
||||||
|
|
||||||
return positionChanges.toFloatArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
|
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
|
||||||
if (!useKeyboardEvents && !controllerEventsEnabled) {
|
if (!useKeyboardEvents) {
|
||||||
return super.onGenericMotionEvent(event)
|
return super.onGenericMotionEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,41 +340,9 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
|
|
||||||
queueEvent {
|
queueEvent {
|
||||||
sendNextEventTimestamp(timestamp)
|
sendNextEventTimestamp(timestamp)
|
||||||
|
|
||||||
if (useKeyboardEvents) {
|
|
||||||
GeodeUtils.nativeActionScroll(scrollX, scrollY)
|
GeodeUtils.nativeActionScroll(scrollX, scrollY)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (controllerEventsEnabled) {
|
|
||||||
GeodeUtils.internalScrollEvent(timestamp, event.deviceId, event.source, scrollX, scrollY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controllerEventsEnabled && event?.action == MotionEvent.ACTION_MOVE && event.source and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK) {
|
|
||||||
val (motionLeftX, motionLeftY) = getJoystickMotion(event, MotionEvent.AXIS_X, MotionEvent.AXIS_Y)
|
|
||||||
val (motionRightX, motionRightY) = getJoystickMotion(event, MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ)
|
|
||||||
val (motionDPadX, motionDPadY) = getJoystickMotion(event, MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y)
|
|
||||||
|
|
||||||
val motionLTrigger = getJoystickMotion(event, MotionEvent.AXIS_LTRIGGER)
|
|
||||||
val motionRTrigger = getJoystickMotion(event, MotionEvent.AXIS_RTRIGGER)
|
|
||||||
|
|
||||||
// i'm sure this is great for performance
|
|
||||||
val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
||||||
event.eventTimeNanos else event.eventTime * MS_TO_NS
|
|
||||||
|
|
||||||
queueEvent {
|
|
||||||
GeodeUtils.internalJoystickEvent(
|
|
||||||
timestamp, event.deviceId, event.source,
|
|
||||||
motionLeftX, motionLeftY,
|
|
||||||
motionRightX, motionRightY,
|
|
||||||
motionDPadX, motionDPadY,
|
|
||||||
motionLTrigger, motionRTrigger
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,40 +365,4 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
fun deleteBackward() {
|
fun deleteBackward() {
|
||||||
queueEvent { cocos2dxRenderer.handleDeleteBackward() }
|
queueEvent { cocos2dxRenderer.handleDeleteBackward() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enableControllerEvents() {
|
|
||||||
if (controllerEventsEnabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
controllerEventsEnabled = true
|
|
||||||
|
|
||||||
val inputManager = context.getSystemService(Context.INPUT_SERVICE) as InputManager
|
|
||||||
inputManager.registerInputDeviceListener(InputListener, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun disableControllerEvents() {
|
|
||||||
if (!controllerEventsEnabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
controllerEventsEnabled = true
|
|
||||||
|
|
||||||
val inputManager = context.getSystemService(Context.INPUT_SERVICE) as InputManager
|
|
||||||
inputManager.unregisterInputDeviceListener(InputListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
object InputListener : InputManager.InputDeviceListener {
|
|
||||||
override fun onInputDeviceAdded(deviceId: Int) {
|
|
||||||
GeodeUtils.inputDeviceAdded(deviceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInputDeviceChanged(deviceId: Int) {
|
|
||||||
GeodeUtils.inputDeviceChanged(deviceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInputDeviceRemoved(deviceId: Int) {
|
|
||||||
GeodeUtils.inputDeviceRemoved(deviceId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -40,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="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="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>
|
|
||||||
@@ -64,18 +64,13 @@
|
|||||||
<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_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_self_update_progress">Updating Geode Launcher…</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_error">Geode cannot be used.</string>
|
<string name="launcher_cancelled_error">Could not start Geode.</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>
|
||||||
@@ -98,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>
|
||||||
@@ -116,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>
|
||||||
@@ -151,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>
|
||||||
@@ -210,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>
|
||||||
@@ -228,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>
|
||||||
@@ -237,10 +217,6 @@
|
|||||||
<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_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>
|
||||||
@@ -286,6 +262,12 @@
|
|||||||
<string name="preference_override_exceptions_description">Uses exception_symbols.txt to patch</string>
|
<string name="preference_override_exceptions_description">Uses exception_symbols.txt to patch</string>
|
||||||
<string name="preference_use_index_update_api">Use index release API</string>
|
<string name="preference_use_index_update_api">Use index release API</string>
|
||||||
|
|
||||||
|
<string name="preference_development_server_name">Enable development server</string>
|
||||||
|
<string name="preference_development_server_description">Opens a public development server on port 9043.</string>
|
||||||
|
|
||||||
|
<string name="notification_channel_development_server">Development Server Service</string>
|
||||||
|
<string name="notification_channel_development_server_description">Indicates the development server is currently active.</string>
|
||||||
|
|
||||||
<!-- CrashLogsActivity strings -->
|
<!-- CrashLogsActivity strings -->
|
||||||
<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>
|
||||||
@@ -298,6 +280,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>
|
||||||
+4
-3
@@ -1,11 +1,12 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
val composeBOM by extra("2026.01.01")
|
val composeBOM by extra("2025.12.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.0" apply false
|
id("com.android.application") version "8.13.2" apply false
|
||||||
id("com.android.library") version "9.0.0" apply false
|
id("com.android.library") version "8.13.2" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.3.0" apply false
|
||||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.3.0" apply false
|
id("org.jetbrains.kotlin.plugin.serialization") version "2.3.0" apply false
|
||||||
id("org.jetbrains.kotlin.plugin.compose") version "2.3.0" apply false
|
id("org.jetbrains.kotlin.plugin.compose") version "2.3.0" apply false
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -1,7 +1,6 @@
|
|||||||
#Wed Jan 28 04:12:33 MST 2026
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
Reference in new issue
Block a user