mirror of
https://github.com/geode-sdk/android-launcher.git
synced 2026-10-22 16:42:38 +00:00
Compare commits
25
Commits
1.6.1
...
controller-api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
915e232011 | ||
|
|
cf4cd8335e | ||
|
|
2797e52683 | ||
|
|
69b00fecf5 | ||
|
|
f0eddd1652 | ||
|
|
d7190fa928 | ||
|
|
8e8a56da2e | ||
|
|
b418e154da | ||
|
|
cd306a5b5e | ||
|
|
fb9a6d1912 | ||
|
|
e276e302ab | ||
|
|
d7c779fce3 | ||
|
|
1e0dcc91ec | ||
|
|
266055e6d5 | ||
|
|
ab8e3f83d2 | ||
|
|
a75d68e5e0 | ||
|
|
13191dc66f | ||
|
|
b7f378b947 | ||
|
|
25367d5650 | ||
|
|
aa51c201e5 | ||
|
|
0b92f4a318 | ||
|
|
7d75825d01 | ||
|
|
c77271f45c | ||
|
|
09bb28d66f | ||
|
|
157952b6f7 |
No files matched your search
+21
-17
@@ -1,3 +1,5 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
val composeBOM: String by rootProject.extra
|
val composeBOM: String by rootProject.extra
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
@@ -68,8 +70,10 @@ android {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlin {
|
||||||
jvmTarget = "1.8"
|
compilerOptions {
|
||||||
|
jvmTarget = JvmTarget.JVM_1_8
|
||||||
|
}
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
@@ -92,24 +96,24 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation (platform("androidx.compose:compose-bom:$composeBOM"))
|
implementation (platform("androidx.compose:compose-bom:$composeBOM"))
|
||||||
implementation ("androidx.core:core-ktx:1.16.0")
|
implementation ("androidx.core:core-ktx:1.17.0")
|
||||||
implementation ("androidx.compose.ui:ui")
|
implementation ("androidx.compose.ui:ui")
|
||||||
implementation ("androidx.compose.material3:material3")
|
implementation ("androidx.compose.material3:material3")
|
||||||
implementation ("androidx.compose.ui:ui-tooling-preview")
|
implementation ("androidx.compose.ui:ui-tooling-preview")
|
||||||
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
|
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.9.3")
|
||||||
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
|
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.3")
|
||||||
implementation ("androidx.activity:activity-compose:1.10.1")
|
implementation ("androidx.activity:activity-compose:1.11.0")
|
||||||
implementation ("androidx.activity:activity-ktx:1.10.1")
|
implementation ("androidx.activity:activity-ktx:1.11.0")
|
||||||
implementation ("androidx.appcompat:appcompat:1.7.0")
|
implementation ("androidx.appcompat:appcompat:1.7.1")
|
||||||
implementation ("androidx.documentfile:documentfile:1.0.1")
|
implementation ("androidx.documentfile:documentfile:1.1.0")
|
||||||
implementation ("com.squareup.okio:okio:3.11.0")
|
implementation ("com.squareup.okio:okio:3.16.0")
|
||||||
implementation ("com.squareup.okhttp3:okhttp:4.12.0")
|
implementation ("com.squareup.okhttp3:okhttp:5.1.0")
|
||||||
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
|
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||||
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.8.1")
|
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.9.0")
|
||||||
implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
|
implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
|
||||||
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.33.0")
|
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.37.0")
|
||||||
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.33.0")
|
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.37.0")
|
||||||
implementation ("androidx.browser:browser:1.8.0")
|
implementation ("androidx.browser:browser:1.9.0")
|
||||||
debugImplementation ("androidx.compose.ui:ui-tooling")
|
debugImplementation ("androidx.compose.ui:ui-tooling")
|
||||||
coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.1.5")
|
coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.1.5")
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
<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.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.LIGHTS" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.wifi"
|
android:name="android.hardware.wifi"
|
||||||
@@ -29,6 +30,9 @@
|
|||||||
<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"
|
||||||
@@ -56,14 +60,21 @@
|
|||||||
android:exported="false"
|
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" />
|
||||||
|
<activity
|
||||||
|
android:name=".preferences.CrashLogsActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/title_activity_crash_logs"
|
||||||
|
android:theme="@style/Theme.GeodeLauncher" />
|
||||||
|
<activity
|
||||||
|
android:name=".preferences.ApplicationIconActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/title_activity_application_icon"
|
||||||
|
android:theme="@style/Theme.GeodeLauncher" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".preferences.SettingsActivity"
|
android:name=".preferences.SettingsActivity"
|
||||||
android:exported="false"
|
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">
|
||||||
<meta-data
|
|
||||||
android:name="android.app.lib_name"
|
|
||||||
android:value="" />
|
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".GeometryDashActivity"
|
android:name=".GeometryDashActivity"
|
||||||
@@ -83,11 +94,49 @@
|
|||||||
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>
|
||||||
</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
|
||||||
|
android:name=".MainActivityTrans"
|
||||||
|
android:enabled="false"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:icon="@mipmap/ic_launcher_trans"
|
||||||
|
android:targetActivity=".MainActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity-alias>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".UserDirectoryProvider"
|
android:name=".UserDirectoryProvider"
|
||||||
android:authorities="com.geode.launcher.user"
|
android:authorities="com.geode.launcher.user"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ option(USE_TULIPHOOK "enables choice of hooking library" OFF)
|
|||||||
if (USE_TULIPHOOK)
|
if (USE_TULIPHOOK)
|
||||||
FetchContent_Declare(TulipHook
|
FetchContent_Declare(TulipHook
|
||||||
GIT_REPOSITORY https://github.com/geode-sdk/TulipHook.git
|
GIT_REPOSITORY https://github.com/geode-sdk/TulipHook.git
|
||||||
GIT_TAG 9aa1f95091ebac18c657d57b755f59d00fe6f1f8
|
GIT_TAG v3.1.4
|
||||||
)
|
)
|
||||||
|
|
||||||
set(TULIP_LINK_SOURCE ON CACHE INTERNAL "")
|
set(TULIP_LINK_SOURCE ON CACHE INTERNAL "")
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -53,11 +54,13 @@ 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.text.style.TextAlign
|
||||||
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.launcherTitleStyle
|
||||||
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
|
||||||
@@ -93,6 +96,7 @@ class AltMainActivity : ComponentActivity() {
|
|||||||
val theme = Theme.fromInt(themeOption)
|
val theme = Theme.fromInt(themeOption)
|
||||||
|
|
||||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||||
|
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||||
|
|
||||||
val launchViewModel = viewModel<LaunchViewModel>(factory = LaunchViewModel.Factory)
|
val launchViewModel = viewModel<LaunchViewModel>(factory = LaunchViewModel.Factory)
|
||||||
|
|
||||||
@@ -101,7 +105,7 @@ class AltMainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CompositionLocalProvider(LocalTheme provides theme) {
|
CompositionLocalProvider(LocalTheme provides theme) {
|
||||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
@@ -407,15 +411,18 @@ fun GeodeLogo(modifier: Modifier = Modifier) {
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
Image(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.geode_logo),
|
painter = painterResource(id = R.drawable.geode_monochrome),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(76.dp, 76.dp)
|
modifier = Modifier
|
||||||
|
.size(64.dp, 64.dp),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.launcher_title),
|
stringResource(R.string.launcher_title),
|
||||||
style = MaterialTheme.typography.displayMedium,
|
style = launcherTitleStyle,
|
||||||
modifier = Modifier.padding(12.dp)
|
fontSize = 64.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(12.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ import android.util.Log
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.window.OnBackInvokedDispatcher
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.widget.AppCompatEditText
|
||||||
import androidx.compose.ui.platform.ComposeView
|
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
|
||||||
@@ -32,7 +33,6 @@ import com.geode.launcher.utils.GamePackageUtils
|
|||||||
import com.geode.launcher.utils.GeodeUtils
|
import com.geode.launcher.utils.GeodeUtils
|
||||||
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 org.cocos2dx.lib.Cocos2dxEditText
|
|
||||||
import org.cocos2dx.lib.Cocos2dxGLSurfaceView
|
import org.cocos2dx.lib.Cocos2dxGLSurfaceView
|
||||||
import org.cocos2dx.lib.Cocos2dxHelper
|
import org.cocos2dx.lib.Cocos2dxHelper
|
||||||
import org.cocos2dx.lib.Cocos2dxRenderer
|
import org.cocos2dx.lib.Cocos2dxRenderer
|
||||||
@@ -123,12 +123,12 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT) {
|
override fun handleOnBackPressed() {
|
||||||
mGLSurfaceView?.sendKeyBack()
|
mGLSurfaceView?.sendKeyBack()
|
||||||
}
|
}
|
||||||
mGLSurfaceView?.manualBackEvents = true
|
})
|
||||||
}
|
mGLSurfaceView?.manualBackEvents = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createVersionFile() {
|
private fun createVersionFile() {
|
||||||
@@ -406,7 +406,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
)
|
)
|
||||||
val editText = Cocos2dxEditText(this)
|
val editText = AppCompatEditText(this)
|
||||||
editText.layoutParams = editTextLayoutParams
|
editText.layoutParams = editTextLayoutParams
|
||||||
frameLayout.addView(editText)
|
frameLayout.addView(editText)
|
||||||
|
|
||||||
@@ -443,7 +443,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
renderer.limitFrameRate(frameRate)
|
renderer.limitFrameRate(frameRate)
|
||||||
}
|
}
|
||||||
|
|
||||||
editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
||||||
glSurfaceView.cocos2dxEditText = editText
|
glSurfaceView.cocos2dxEditText = editText
|
||||||
|
|
||||||
return frameLayout
|
return frameLayout
|
||||||
@@ -588,6 +588,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
mGLSurfaceView?.sendTimestampEvents = true
|
mGLSurfaceView?.sendTimestampEvents = true
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
GeodeUtils.CAPABILITY_RICH_INPUT -> {
|
||||||
|
mGLSurfaceView?.enableControllerEvents()
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import com.geode.launcher.updater.ReleaseViewModel
|
|||||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||||
import com.geode.launcher.ui.theme.LocalTheme
|
import com.geode.launcher.ui.theme.LocalTheme
|
||||||
import com.geode.launcher.ui.theme.Theme
|
import com.geode.launcher.ui.theme.Theme
|
||||||
|
import com.geode.launcher.ui.theme.launcherTitleStyle
|
||||||
import com.geode.launcher.utils.Constants
|
import com.geode.launcher.utils.Constants
|
||||||
import com.geode.launcher.utils.LaunchUtils
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
import com.geode.launcher.utils.PreferenceUtils
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
@@ -76,9 +77,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
val theme = Theme.fromInt(themeOption)
|
val theme = Theme.fromInt(themeOption)
|
||||||
|
|
||||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||||
|
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||||
|
|
||||||
CompositionLocalProvider(LocalTheme provides theme) {
|
CompositionLocalProvider(LocalTheme provides theme) {
|
||||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.background
|
color = MaterialTheme.colorScheme.background
|
||||||
@@ -163,7 +165,7 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
context.getString(R.string.launcher_title),
|
context.getString(R.string.launcher_title),
|
||||||
fontSize = 32.sp,
|
style = launcherTitleStyle,
|
||||||
modifier = Modifier.padding(12.dp)
|
modifier = Modifier.padding(12.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package com.geode.launcher.log
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.initializer
|
||||||
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
|
data class CrashDump @OptIn(ExperimentalTime::class) constructor(
|
||||||
|
val filename: String,
|
||||||
|
val lastModified: Instant
|
||||||
|
)
|
||||||
|
|
||||||
|
class CrashViewModel(private val application: Application) : ViewModel() {
|
||||||
|
companion object {
|
||||||
|
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||||
|
initializer {
|
||||||
|
val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
|
||||||
|
|
||||||
|
CrashViewModel(
|
||||||
|
application = application
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _lineState = mutableStateListOf<CrashDump>()
|
||||||
|
val lineState = _lineState
|
||||||
|
|
||||||
|
private val _isLoading = MutableStateFlow(false)
|
||||||
|
val isLoading = _isLoading.asStateFlow()
|
||||||
|
|
||||||
|
private var failedToLoad = false
|
||||||
|
|
||||||
|
private var logJob: Job? = null
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
|
private fun getCrashList(): List<CrashDump> {
|
||||||
|
val crashDirectory = LaunchUtils.getCrashDirectory(application)
|
||||||
|
if (!crashDirectory.exists()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val children = crashDirectory.listFiles {
|
||||||
|
// ignore indicator files (including old file)
|
||||||
|
_, name -> name != LaunchUtils.CRASH_INDICATOR_NAME && name != "last-pid"
|
||||||
|
} ?: return emptyList()
|
||||||
|
|
||||||
|
return children
|
||||||
|
.map {
|
||||||
|
CrashDump(
|
||||||
|
filename = it.name,
|
||||||
|
lastModified = Instant.fromEpochMilliseconds(it.lastModified())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.sortedByDescending { it.lastModified }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearCrashes() {
|
||||||
|
logJob?.cancel()
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val crashDirectory = LaunchUtils.getCrashDirectory(application)
|
||||||
|
if (crashDirectory.exists()) {
|
||||||
|
val files = crashDirectory.listFiles()
|
||||||
|
files?.forEach {
|
||||||
|
it.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_lineState.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadLogs() {
|
||||||
|
_isLoading.value = true
|
||||||
|
|
||||||
|
logJob?.cancel()
|
||||||
|
logJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val lines = getCrashList()
|
||||||
|
|
||||||
|
_lineState.clear()
|
||||||
|
_lineState.addAll(lines)
|
||||||
|
|
||||||
|
_isLoading.value = false
|
||||||
|
|
||||||
|
logJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCrash(filename: String) {
|
||||||
|
logJob?.cancel()
|
||||||
|
|
||||||
|
val crashPath = File(LaunchUtils.getCrashDirectory(application), filename)
|
||||||
|
if (crashPath.exists()) {
|
||||||
|
crashPath.delete()
|
||||||
|
loadLogs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadLogs()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
package com.geode.launcher.log
|
package com.geode.launcher.log
|
||||||
|
|
||||||
import kotlinx.datetime.Clock
|
import kotlin.time.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlin.time.Instant
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.toJavaInstant
|
|
||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
import kotlin.time.toJavaInstant
|
||||||
|
|
||||||
fun BufferedSource.readCChar(): Char {
|
fun BufferedSource.readCChar(): Char {
|
||||||
return this.readUtf8CodePoint().toChar()
|
return this.readUtf8CodePoint().toChar()
|
||||||
@@ -115,7 +116,7 @@ enum class LogPriority {
|
|||||||
/**
|
/**
|
||||||
* Represents a log entry from logcat.
|
* Represents a log entry from logcat.
|
||||||
*/
|
*/
|
||||||
data class LogLine(
|
data class LogLine @OptIn(ExperimentalTime::class) constructor(
|
||||||
val process: ProcessInformation,
|
val process: ProcessInformation,
|
||||||
val time: Instant,
|
val time: Instant,
|
||||||
val logId: Int,
|
val logId: Int,
|
||||||
@@ -134,6 +135,7 @@ data class LogLine(
|
|||||||
else -> throw IOException("LogLine::fromInputStream: unknown format for (headerSize = $size)")
|
else -> throw IOException("LogLine::fromInputStream: unknown format for (headerSize = $size)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
fun fromBufferedSource(source: BufferedSource): LogLine {
|
fun fromBufferedSource(source: BufferedSource): LogLine {
|
||||||
/*
|
/*
|
||||||
// from android <liblog/include/log/log_read.h>
|
// from android <liblog/include/log/log_read.h>
|
||||||
@@ -200,6 +202,7 @@ data class LogLine(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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(),
|
||||||
@@ -210,12 +213,14 @@ data class LogLine(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
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}"
|
||||||
|
|||||||
@@ -248,13 +248,18 @@ fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
|
|||||||
Text(stringResource(R.string.launcher_error_export_crash))
|
Text(stringResource(R.string.launcher_error_export_crash))
|
||||||
}
|
}
|
||||||
|
|
||||||
FilledTonalButton(onClick = { onShowLogs(context) }) {
|
val developerSettingsEnabled = remember {
|
||||||
Icon(
|
PreferenceUtils.get(context).getBoolean(PreferenceUtils.Key.DEVELOPER_MODE)
|
||||||
painterResource(R.drawable.icon_description),
|
}
|
||||||
contentDescription = null
|
if (developerSettingsEnabled) {
|
||||||
)
|
FilledTonalButton(onClick = { onShowLogs(context) }) {
|
||||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
Icon(
|
||||||
Text(stringResource(R.string.launcher_error_view_logs))
|
painterResource(R.drawable.icon_description),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
Text(stringResource(R.string.launcher_error_view_logs))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.platform.LocalUriHandler
|
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.style.TextAlign
|
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
|
||||||
@@ -59,13 +60,14 @@ 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.datetime.toJavaInstant
|
|
||||||
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.ConnectException
|
||||||
import java.net.UnknownHostException
|
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
|
||||||
|
|
||||||
fun clearDownloadedApks(context: Context) {
|
fun clearDownloadedApks(context: Context) {
|
||||||
// technically we should be using the activity results but it was too inconsistent for my liking
|
// technically we should be using the activity results but it was too inconsistent for my liking
|
||||||
@@ -135,7 +137,7 @@ fun installLauncherUpdate(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -196,9 +198,11 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
|||||||
Markdown(
|
Markdown(
|
||||||
content = nextRelease.release.body.replace("\r", ""),
|
content = nextRelease.release.body.replace("\r", ""),
|
||||||
typography = markdownTypography(
|
typography = markdownTypography(
|
||||||
link = MaterialTheme.typography.bodyLarge.copy(
|
textLink = TextLinkStyles(
|
||||||
textDecoration = TextDecoration.Underline,
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
color = MaterialTheme.colorScheme.primary,
|
textDecoration = TextDecoration.Underline,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
).toSpanStyle()
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
flavour = GFMFlavourDescriptor(),
|
flavour = GFMFlavourDescriptor(),
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
package com.geode.launcher.preferences
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.OnBackPressedDispatcher
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.geode.launcher.R
|
||||||
|
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||||
|
import com.geode.launcher.ui.theme.LocalTheme
|
||||||
|
import com.geode.launcher.ui.theme.Theme
|
||||||
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import com.geode.launcher.utils.ApplicationIcon
|
||||||
|
import com.geode.launcher.utils.ApplicationIconDetails
|
||||||
|
import com.geode.launcher.utils.IconUtils
|
||||||
|
import com.geode.launcher.utils.adaptiveIconPainterResource
|
||||||
|
|
||||||
|
class ApplicationIconActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContent {
|
||||||
|
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
|
||||||
|
val theme = Theme.fromInt(themeOption)
|
||||||
|
|
||||||
|
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||||
|
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalTheme provides theme) {
|
||||||
|
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
color = MaterialTheme.colorScheme.background
|
||||||
|
) {
|
||||||
|
ApplicationIconScreen(onBackPressedDispatcher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSelectIcon(context: Context, selectedIcon: ApplicationIconDetails) {
|
||||||
|
val packageManager = context.packageManager
|
||||||
|
|
||||||
|
try {
|
||||||
|
ApplicationIcon.entries.forEach {
|
||||||
|
val iconData = IconUtils.getIconDetails(it)
|
||||||
|
|
||||||
|
packageManager.setComponentEnabledSetting(
|
||||||
|
ComponentName(context, "${context.packageName}.${iconData.component}"),
|
||||||
|
if (iconData.id == selectedIcon.id)
|
||||||
|
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||||
|
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||||
|
PackageManager.DONT_KILL_APP
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
Toast.makeText(context, R.string.application_icon_set_failed, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
PreferenceUtils.get(context).setString(PreferenceUtils.Key.SELECTED_ICON, selectedIcon.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun IconCard(iconData: ApplicationIconDetails, selected: Boolean, modifier: Modifier = Modifier) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 1.dp)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.then(modifier)
|
||||||
|
.selectable(
|
||||||
|
selected = selected,
|
||||||
|
onClick = {
|
||||||
|
onSelectIcon(context, iconData)
|
||||||
|
},
|
||||||
|
role = Role.RadioButton
|
||||||
|
)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
Arrangement.SpaceBetween,
|
||||||
|
Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
adaptiveIconPainterResource(iconData.iconId),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Text(stringResource(iconData.nameId))
|
||||||
|
}
|
||||||
|
|
||||||
|
RadioButton(
|
||||||
|
selected = selected,
|
||||||
|
onClick = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ApplicationIconScreen(
|
||||||
|
onBackPressedDispatcher: OnBackPressedDispatcher?,
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
|
|
||||||
|
var showMoreOptions by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
Column {
|
||||||
|
TopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.back_icon_alt)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.title_activity_application_icon),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
val selectedIcon by PreferenceUtils.useStringPreference(PreferenceUtils.Key.SELECTED_ICON)
|
||||||
|
|
||||||
|
Column(Modifier
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
) {
|
||||||
|
ApplicationIcon.entries.forEach {
|
||||||
|
val iconData = IconUtils.getIconDetails(it)
|
||||||
|
|
||||||
|
IconCard(iconData, selectedIcon == iconData.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,6 +70,7 @@ import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
|||||||
import com.geode.launcher.ui.theme.LocalTheme
|
import com.geode.launcher.ui.theme.LocalTheme
|
||||||
import com.geode.launcher.ui.theme.Theme
|
import com.geode.launcher.ui.theme.Theme
|
||||||
import com.geode.launcher.ui.theme.Typography
|
import com.geode.launcher.ui.theme.Typography
|
||||||
|
import com.geode.launcher.ui.theme.robotoMonoFamily
|
||||||
import com.geode.launcher.utils.PreferenceUtils
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -83,9 +84,10 @@ class ApplicationLogsActivity : ComponentActivity() {
|
|||||||
val theme = Theme.fromInt(themeOption)
|
val theme = Theme.fromInt(themeOption)
|
||||||
|
|
||||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||||
|
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||||
|
|
||||||
CompositionLocalProvider(LocalTheme provides theme) {
|
CompositionLocalProvider(LocalTheme provides theme) {
|
||||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.background
|
color = MaterialTheme.colorScheme.background
|
||||||
@@ -166,12 +168,12 @@ fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
|
|||||||
"[${logLine.priority.toChar()}] ${logLine.formattedTime}",
|
"[${logLine.priority.toChar()}] ${logLine.formattedTime}",
|
||||||
style = Typography.labelSmall,
|
style = Typography.labelSmall,
|
||||||
color = getLogColor(logLine.priority),
|
color = getLogColor(logLine.priority),
|
||||||
fontFamily = FontFamily.Monospace
|
fontFamily = robotoMonoFamily
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
"[${logLine.tag}]: ${logLine.messageTrimmed}",
|
"[${logLine.tag}]: ${logLine.messageTrimmed}",
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = robotoMonoFamily,
|
||||||
style = Typography.bodyMedium
|
style = Typography.bodyMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -242,21 +244,12 @@ fun ApplicationLogsScreen(
|
|||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val data = logViewModel.getLogData()
|
saveLauncher.launch(context.getString(R.string.application_logs_default_filename))
|
||||||
|
|
||||||
val sendIntent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
putExtra(Intent.EXTRA_TEXT, data)
|
|
||||||
type = "text/plain"
|
|
||||||
}
|
|
||||||
|
|
||||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
|
||||||
|
|
||||||
context.startActivity(shareIntent)
|
|
||||||
}
|
}
|
||||||
}, enabled = !logLines.isEmpty()) {
|
}, enabled = !logLines.isEmpty()) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Share,
|
painterResource(R.drawable.icon_save),
|
||||||
contentDescription = stringResource(R.string.application_logs_share)
|
contentDescription = stringResource(R.string.application_logs_export),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
|
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
|
||||||
@@ -309,23 +302,6 @@ fun ApplicationLogsScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
DropdownMenuItem(
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_save),
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(stringResource(R.string.application_logs_export))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
saveLauncher.launch(context.getString(R.string.application_logs_default_filename))
|
|
||||||
showMoreOptions = false
|
|
||||||
},
|
|
||||||
enabled = !logLines.isEmpty()
|
|
||||||
)
|
|
||||||
|
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
package com.geode.launcher.preferences
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import android.widget.Toast
|
||||||
|
import java.text.DateFormat
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.OnBackPressedDispatcher
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material.icons.filled.Share
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.geode.launcher.R
|
||||||
|
import com.geode.launcher.UserDirectoryProvider
|
||||||
|
import com.geode.launcher.log.CrashDump
|
||||||
|
import com.geode.launcher.log.CrashViewModel
|
||||||
|
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||||
|
import com.geode.launcher.ui.theme.LocalTheme
|
||||||
|
import com.geode.launcher.ui.theme.Theme
|
||||||
|
import com.geode.launcher.ui.theme.Typography
|
||||||
|
import com.geode.launcher.ui.theme.robotoMonoFamily
|
||||||
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
|
import java.io.File
|
||||||
|
import java.util.Date
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
import kotlin.time.toJavaInstant
|
||||||
|
|
||||||
|
class CrashLogsActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContent {
|
||||||
|
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
|
||||||
|
val theme = Theme.fromInt(themeOption)
|
||||||
|
|
||||||
|
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||||
|
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalTheme provides theme) {
|
||||||
|
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
color = MaterialTheme.colorScheme.background
|
||||||
|
) {
|
||||||
|
CrashLogsScreen(onBackPressedDispatcher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shareCrash(context: Context, filename: String) {
|
||||||
|
val baseDirectory = LaunchUtils.getBaseDirectory(context)
|
||||||
|
val crashPath = File(LaunchUtils.getCrashDirectory(context), filename)
|
||||||
|
|
||||||
|
if (!crashPath.exists()) {
|
||||||
|
Toast.makeText(context, R.string.launcher_error_export_missing, Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val providerLocation = crashPath.toRelativeString(baseDirectory)
|
||||||
|
val documentsPath = "${UserDirectoryProvider.ROOT}${providerLocation}"
|
||||||
|
|
||||||
|
val uri = DocumentsContract.buildDocumentUri("${context.packageName}.user", documentsPath)
|
||||||
|
|
||||||
|
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
type = "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
context.startActivity(shareIntent)
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
Toast.makeText(context, R.string.no_activity_found, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
|
@Composable
|
||||||
|
fun CrashCard(crashDump: CrashDump, crashViewModel: CrashViewModel, modifier: Modifier = Modifier) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 1.dp, horizontal = 8.dp)
|
||||||
|
.height(64.dp)
|
||||||
|
.then(modifier)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||||
|
.padding(vertical = 8.dp, horizontal = 8.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.SpaceAround,
|
||||||
|
modifier = Modifier.fillMaxHeight().offset(x = 12.dp).weight(0.7f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
crashDump.filename,
|
||||||
|
fontFamily = robotoMonoFamily,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
val formattedLastModified = remember {
|
||||||
|
DateFormat.getDateTimeInstance()
|
||||||
|
.format(
|
||||||
|
Date.from(crashDump.lastModified.toJavaInstant())
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
Text(formattedLastModified, style = MaterialTheme.typography.labelMedium)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(modifier = Modifier.weight(0.3f), horizontalArrangement = Arrangement.End) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
IconButton(onClick = {
|
||||||
|
shareCrash(context, crashDump.filename)
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Share,
|
||||||
|
contentDescription = stringResource(R.string.application_logs_share)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = {
|
||||||
|
crashViewModel.removeCrash(crashDump.filename)
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Delete,
|
||||||
|
contentDescription = stringResource(R.string.application_crashes_delete_one)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun CrashLogsScreen(
|
||||||
|
onBackPressedDispatcher: OnBackPressedDispatcher?,
|
||||||
|
crashViewModel: CrashViewModel = viewModel(factory = CrashViewModel.Factory)
|
||||||
|
) {
|
||||||
|
val logLines = crashViewModel.lineState
|
||||||
|
val isLoading by crashViewModel.isLoading.collectAsState()
|
||||||
|
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
|
|
||||||
|
var showMoreOptions by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
Column {
|
||||||
|
TopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.back_icon_alt)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.MoreVert,
|
||||||
|
contentDescription = stringResource(R.string.application_logs_more)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showMoreOptions,
|
||||||
|
onDismissRequest = { showMoreOptions = false }
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_delete),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.application_logs_delete))
|
||||||
|
}, onClick = {
|
||||||
|
crashViewModel.clearCrashes()
|
||||||
|
showMoreOptions = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.title_activity_crash_logs),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
LazyColumn(
|
||||||
|
Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
if (!isLoading && logLines.isEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.application_crashes_no_dumps),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = Typography.bodyLarge,
|
||||||
|
modifier = Modifier.fillMaxSize().padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val itemsSize = logLines.size
|
||||||
|
|
||||||
|
itemsIndexed(
|
||||||
|
logLines,
|
||||||
|
key = { index, line -> line.filename }
|
||||||
|
) { index, line ->
|
||||||
|
val shape = RoundedCornerShape(
|
||||||
|
topStart = if (index == 0) 16.dp else 4.dp,
|
||||||
|
topEnd = if (index == 0) 16.dp else 4.dp,
|
||||||
|
bottomStart = if (index + 1 == itemsSize) 16.dp else 4.dp,
|
||||||
|
bottomEnd = if (index + 1 == itemsSize) 16.dp else 4.dp
|
||||||
|
)
|
||||||
|
|
||||||
|
CrashCard(
|
||||||
|
line,
|
||||||
|
crashViewModel,
|
||||||
|
modifier = Modifier.clip(shape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
fun CrashLogsPreview() {
|
||||||
|
GeodeLauncherTheme {
|
||||||
|
CrashLogsScreen(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
package com.geode.launcher.preferences
|
package com.geode.launcher.preferences
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.OnBackPressedDispatcher
|
import androidx.activity.OnBackPressedDispatcher
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
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.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.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
|
||||||
@@ -32,6 +39,8 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -39,7 +48,7 @@ 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
|
||||||
import com.geode.launcher.ui.theme.Theme
|
import com.geode.launcher.ui.theme.Theme
|
||||||
import com.geode.launcher.utils.LabelledText
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
import com.geode.launcher.utils.PreferenceUtils
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
|
|
||||||
class DeveloperSettingsActivity : ComponentActivity() {
|
class DeveloperSettingsActivity : ComponentActivity() {
|
||||||
@@ -52,9 +61,10 @@ class DeveloperSettingsActivity : ComponentActivity() {
|
|||||||
val theme = Theme.fromInt(themeOption)
|
val theme = Theme.fromInt(themeOption)
|
||||||
|
|
||||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||||
|
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||||
|
|
||||||
CompositionLocalProvider(LocalTheme provides theme) {
|
CompositionLocalProvider(LocalTheme provides theme) {
|
||||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.background
|
color = MaterialTheme.colorScheme.background
|
||||||
@@ -67,6 +77,30 @@ class DeveloperSettingsActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onOpenFolder(context: Context) {
|
||||||
|
val file = LaunchUtils.getBaseDirectory(context)
|
||||||
|
val clipboardManager =
|
||||||
|
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
clipboardManager.setPrimaryClip(
|
||||||
|
ClipData.newPlainText(
|
||||||
|
context.getString(R.string.export_folder_tag),
|
||||||
|
file.path
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.text_copied),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onOpenLogs(context: Context) {
|
||||||
|
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
|
||||||
|
context.startActivity(launchIntent)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||||
@@ -93,6 +127,7 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.background),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,16 +142,12 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
|||||||
|
|
||||||
val developerMode by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
|
val developerMode by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
|
||||||
|
|
||||||
SettingsCard(
|
OptionsGroup {
|
||||||
title = stringResource(R.string.preference_developer_mode),
|
SettingsCard(
|
||||||
preferenceKey = PreferenceUtils.Key.DEVELOPER_MODE,
|
title = stringResource(R.string.preference_developer_mode),
|
||||||
)
|
preferenceKey = PreferenceUtils.Key.DEVELOPER_MODE,
|
||||||
|
)
|
||||||
InlineText(
|
}
|
||||||
stringResource(R.string.preference_developer_mode_about)
|
|
||||||
)
|
|
||||||
|
|
||||||
HorizontalDivider()
|
|
||||||
|
|
||||||
if (developerMode) {
|
if (developerMode) {
|
||||||
OptionsGroup(title = stringResource(R.string.preference_category_gameplay)) {
|
OptionsGroup(title = stringResource(R.string.preference_category_gameplay)) {
|
||||||
@@ -145,12 +176,27 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
|||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionsGroup(title = stringResource(R.string.preference_category_updater)) {
|
OptionsGroup(stringResource(R.string.preference_category_developer)) {
|
||||||
SettingsCard(
|
val context = LocalContext.current
|
||||||
title = stringResource(R.string.preference_use_index_update_api),
|
OptionsButton(
|
||||||
preferenceKey = PreferenceUtils.Key.USE_INDEX_API
|
title = stringResource(R.string.preferences_view_logs),
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_description),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { onOpenLogs(context) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
OptionsButton(
|
||||||
|
title = stringResource(R.string.preferences_copy_external_button),
|
||||||
|
description = LaunchUtils.getBaseDirectory(context).path,
|
||||||
|
onClick = { onOpenFolder(context) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionsGroup(title = stringResource(R.string.preference_category_updater)) {
|
||||||
SettingsSelectCard(
|
SettingsSelectCard(
|
||||||
title = stringResource(R.string.preference_release_channel_tag_name),
|
title = stringResource(R.string.preference_release_channel_tag_name),
|
||||||
dialogTitle = stringResource(R.string.preference_release_channel_select),
|
dialogTitle = stringResource(R.string.preference_release_channel_select),
|
||||||
@@ -161,16 +207,6 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
|||||||
2 to stringResource(R.string.preference_release_channel_nightly),
|
2 to stringResource(R.string.preference_release_channel_nightly),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
ElevatedCard(modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)) {
|
|
||||||
LabelledText(
|
|
||||||
label = stringResource(R.string.preference_developer_update_notice),
|
|
||||||
icon = {
|
|
||||||
Icon(Icons.Filled.Info, null)
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionsGroup(title = stringResource(R.string.preference_category_testing)) {
|
OptionsGroup(title = stringResource(R.string.preference_category_testing)) {
|
||||||
@@ -185,6 +221,8 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
|||||||
ProfileSelectCard()
|
ProfileSelectCard()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,6 @@ package com.geode.launcher.preferences
|
|||||||
|
|
||||||
import android.app.UiModeManager
|
import android.app.UiModeManager
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -25,23 +23,23 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
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
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
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.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
|
||||||
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.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
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
|
||||||
@@ -53,8 +51,10 @@ import com.geode.launcher.updater.ReleaseViewModel
|
|||||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||||
import com.geode.launcher.ui.theme.LocalTheme
|
import com.geode.launcher.ui.theme.LocalTheme
|
||||||
import com.geode.launcher.ui.theme.Theme
|
import com.geode.launcher.ui.theme.Theme
|
||||||
|
import com.geode.launcher.utils.ApplicationIcon
|
||||||
import com.geode.launcher.utils.Constants
|
import com.geode.launcher.utils.Constants
|
||||||
import com.geode.launcher.utils.GamePackageUtils
|
import com.geode.launcher.utils.GamePackageUtils
|
||||||
|
import com.geode.launcher.utils.IconUtils
|
||||||
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 java.net.ConnectException
|
import java.net.ConnectException
|
||||||
@@ -71,9 +71,10 @@ class SettingsActivity : ComponentActivity() {
|
|||||||
val theme = Theme.fromInt(themeOption)
|
val theme = Theme.fromInt(themeOption)
|
||||||
|
|
||||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||||
|
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||||
|
|
||||||
CompositionLocalProvider(LocalTheme provides theme) {
|
CompositionLocalProvider(LocalTheme provides theme) {
|
||||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.background
|
color = MaterialTheme.colorScheme.background
|
||||||
@@ -88,25 +89,6 @@ class SettingsActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onOpenFolder(context: Context) {
|
|
||||||
val file = LaunchUtils.getBaseDirectory(context)
|
|
||||||
val clipboardManager =
|
|
||||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
clipboardManager.setPrimaryClip(
|
|
||||||
ClipData.newPlainText(
|
|
||||||
context.getString(R.string.export_folder_tag),
|
|
||||||
file.path
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.text_copied),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onOpenFileManager(context: Context) {
|
fun onOpenFileManager(context: Context) {
|
||||||
// future thoughts: Intent.ACTION_VIEW may not work on some devices
|
// future thoughts: Intent.ACTION_VIEW may not work on some devices
|
||||||
// the "correct" solution is to go through a list of possible intents/applications
|
// the "correct" solution is to go through a list of possible intents/applications
|
||||||
@@ -226,16 +208,36 @@ fun updateTheme(context: Context, theme: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onOpenLogs(context: Context) {
|
|
||||||
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
|
|
||||||
context.startActivity(launchIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onOpenDeveloperOptions(context: Context) {
|
fun onOpenDeveloperOptions(context: Context) {
|
||||||
val launchIntent = Intent(context, DeveloperSettingsActivity::class.java)
|
val launchIntent = Intent(context, DeveloperSettingsActivity::class.java)
|
||||||
context.startActivity(launchIntent)
|
context.startActivity(launchIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DeveloperModeDialog(onDismiss: () -> Unit, onEnable: () -> Unit) {
|
||||||
|
AlertDialog(
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Warning,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = { Text(stringResource(R.string.preference_developer_options_dialog_title)) },
|
||||||
|
text = { Text(stringResource(R.string.preference_developer_mode_about)) },
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.message_box_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onEnable) {
|
||||||
|
Text(stringResource(R.string.message_box_accept))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismissRequest = onDismiss
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
@@ -291,6 +293,7 @@ fun SettingsScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.background),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
content = { innerPadding ->
|
content = { innerPadding ->
|
||||||
@@ -317,8 +320,41 @@ fun SettingsScreen(
|
|||||||
title = stringResource(R.string.preference_black_background_name),
|
title = stringResource(R.string.preference_black_background_name),
|
||||||
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
|
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
SettingsCard(
|
||||||
|
title = stringResource(R.string.preference_disable_styles),
|
||||||
|
preferenceKey = PreferenceUtils.Key.DISABLE_USER_THEME
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val currentIcon by PreferenceUtils.useStringPreference(PreferenceUtils.Key.SELECTED_ICON)
|
||||||
|
val iconName = remember(currentIcon) {
|
||||||
|
IconUtils.getIconDetails(
|
||||||
|
ApplicationIcon.fromId(currentIcon ?: "default")
|
||||||
|
).nameId
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionsButton(
|
||||||
|
title = stringResource(R.string.preference_set_application_icon),
|
||||||
|
onClick = {
|
||||||
|
val launchIntent = Intent(context, ApplicationIconActivity::class.java)
|
||||||
|
context.startActivity(launchIntent)
|
||||||
|
},
|
||||||
|
description = stringResource(iconName),
|
||||||
|
displayInline = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
OptionsButton(
|
OptionsButton(
|
||||||
title = stringResource(R.string.preferences_open_file_manager),
|
title = stringResource(R.string.preferences_open_file_manager),
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_folder),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
onClick = { onOpenFileManager(context) }
|
onClick = { onOpenFileManager(context) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -425,52 +461,72 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val developerModeEnabled by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
|
||||||
|
|
||||||
OptionsGroup(stringResource(R.string.preference_category_developer)) {
|
OptionsGroup(stringResource(R.string.preference_category_developer)) {
|
||||||
|
if (developerModeEnabled) {
|
||||||
|
OptionsButton(
|
||||||
|
title = stringResource(R.string.preference_open_developer_options),
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_data_object),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { onOpenDeveloperOptions(context) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
OptionsButton(
|
OptionsButton(
|
||||||
title = stringResource(R.string.preferences_view_logs),
|
title = stringResource(R.string.preferences_view_crashes),
|
||||||
icon = {
|
icon = {
|
||||||
Icon(painterResource(R.drawable.icon_description), contentDescription = null)
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_bug_report),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onClick = { onOpenLogs(context) }
|
onClick = {
|
||||||
)
|
val launchIntent = Intent(context, CrashLogsActivity::class.java)
|
||||||
OptionsButton(
|
context.startActivity(launchIntent)
|
||||||
title = stringResource(R.string.preference_open_developer_options),
|
}
|
||||||
icon = {
|
|
||||||
Icon(painterResource(R.drawable.icon_data_object), contentDescription = null)
|
|
||||||
},
|
|
||||||
onClick = { onOpenDeveloperOptions(context) }
|
|
||||||
)
|
|
||||||
OptionsButton(
|
|
||||||
title = context.getString(R.string.preferences_copy_external_button),
|
|
||||||
description = LaunchUtils.getBaseDirectory(context).path,
|
|
||||||
onClick = { onOpenFolder(context) }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionsGroup(stringResource(R.string.preference_category_about)) {
|
OptionsGroup(stringResource(R.string.preference_category_about)) {
|
||||||
val clipboard = LocalClipboardManager.current
|
var launcherButtonTapped by remember { mutableIntStateOf(0) }
|
||||||
|
var showDeveloperDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
OptionsButton(
|
OptionsButton(
|
||||||
stringResource(R.string.preference_launcher_version_name),
|
stringResource(R.string.preference_launcher_version_name),
|
||||||
stringResource(R.string.preference_launcher_version_description, BuildConfig.VERSION_NAME),
|
stringResource(R.string.preference_launcher_version_description, BuildConfig.VERSION_NAME),
|
||||||
displayInline = true
|
displayInline = true
|
||||||
) {
|
) {
|
||||||
clipboard.setText(AnnotatedString(context.getString(
|
launcherButtonTapped += 1
|
||||||
R.string.preference_launcher_version,
|
|
||||||
BuildConfig.VERSION_NAME
|
if (launcherButtonTapped >= 7 && !developerModeEnabled) {
|
||||||
)))
|
showDeveloperDialog = true
|
||||||
|
launcherButtonTapped = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionsButton(
|
val context = LocalContext.current
|
||||||
|
if (showDeveloperDialog) {
|
||||||
|
DeveloperModeDialog(
|
||||||
|
onDismiss = {
|
||||||
|
showDeveloperDialog = false
|
||||||
|
},
|
||||||
|
onEnable = {
|
||||||
|
PreferenceUtils.get(context).setBoolean(PreferenceUtils.Key.DEVELOPER_MODE, true)
|
||||||
|
showDeveloperDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionsLabel(
|
||||||
stringResource(R.string.preference_loader_version_name),
|
stringResource(R.string.preference_loader_version_name),
|
||||||
stringResource(R.string.preference_loader_version_description, currentRelease ?: "unknown"),
|
stringResource(R.string.preference_loader_version_description, currentRelease ?: "unknown"),
|
||||||
displayInline = true
|
displayInline = true
|
||||||
) {
|
)
|
||||||
clipboard.setText(AnnotatedString(context.getString(
|
|
||||||
R.string.preference_loader_version,
|
|
||||||
currentRelease ?: "unknown"
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
val gameInstalled = remember {
|
val gameInstalled = remember {
|
||||||
GamePackageUtils.isGameInstalled(context.packageManager)
|
GamePackageUtils.isGameInstalled(context.packageManager)
|
||||||
@@ -489,31 +545,21 @@ fun SettingsScreen(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionsButton(
|
OptionsLabel(
|
||||||
stringResource(R.string.preference_game_version_name),
|
stringResource(R.string.preference_game_version_name),
|
||||||
stringResource(R.string.preference_game_version_description, gameVersion, gameSource),
|
stringResource(R.string.preference_game_version_description, gameVersion, gameSource),
|
||||||
displayInline = true
|
displayInline = true
|
||||||
) {
|
)
|
||||||
clipboard.setText(AnnotatedString(context.getString(
|
|
||||||
R.string.preference_game_version,
|
|
||||||
gameVersion,
|
|
||||||
gameSource
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
OptionsButton(
|
OptionsLabel(
|
||||||
stringResource(R.string.preference_architecture_name),
|
stringResource(R.string.preference_architecture_name),
|
||||||
stringResource(R.string.preference_architecture_description, Build.MODEL, LaunchUtils.applicationArchitecture),
|
stringResource(R.string.preference_architecture_description, Build.MODEL, LaunchUtils.applicationArchitecture),
|
||||||
displayInline = true
|
displayInline = true
|
||||||
) {
|
)
|
||||||
clipboard.setText(AnnotatedString(context.getString(
|
|
||||||
R.string.preference_architecture,
|
|
||||||
Build.MODEL,
|
|
||||||
LaunchUtils.applicationArchitecture
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.geode.launcher.preferences
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.activity.compose.LocalActivity
|
import androidx.activity.compose.LocalActivity
|
||||||
|
import androidx.compose.foundation.background
|
||||||
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.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -46,6 +47,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.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
|
||||||
@@ -82,14 +84,22 @@ fun getSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OptionsGroup(title: String, content: @Composable () -> Unit) {
|
fun OptionsGroup(title: String? = null, content: @Composable () -> Unit) {
|
||||||
Column {
|
Column {
|
||||||
Text(
|
if (title != null) {
|
||||||
title,
|
Text(
|
||||||
style = Typography.titleMedium,
|
title,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
style = Typography.titleMedium,
|
||||||
)
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||||
content()
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -875,6 +885,24 @@ fun OptionsButton(title: String, description: String? = null, icon: (@Composable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OptionsLabel(title: String, description: String? = null, icon: (@Composable () -> Unit)? = null, displayInline: Boolean = false) {
|
||||||
|
OptionsCard(
|
||||||
|
title = {
|
||||||
|
OptionsTitle(
|
||||||
|
title = title,
|
||||||
|
description = description.takeIf { !displayInline },
|
||||||
|
icon = icon
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wrapContent = true
|
||||||
|
) {
|
||||||
|
if (displayInline && description != null) {
|
||||||
|
Text(description, textAlign = TextAlign.End)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@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) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -930,8 +958,12 @@ fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: Stri
|
|||||||
@Composable
|
@Composable
|
||||||
fun OptionsCard(modifier: Modifier = Modifier, wrapContent: Boolean = false, title: @Composable () -> Unit, content: @Composable () -> Unit) {
|
fun OptionsCard(modifier: Modifier = Modifier, wrapContent: Boolean = false, title: @Composable () -> Unit, content: @Composable () -> Unit) {
|
||||||
Row(
|
Row(
|
||||||
modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 1.dp)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.then(modifier)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||||
.defaultMinSize(minHeight = 64.dp)
|
.defaultMinSize(minHeight = 64.dp)
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
Arrangement.SpaceBetween,
|
Arrangement.SpaceBetween,
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.geode.launcher.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.geode.launcher.R
|
||||||
|
|
||||||
|
val robotoMonoFamily = FontFamily(
|
||||||
|
Font(R.font.robotomono_medium, FontWeight.Medium),
|
||||||
|
Font(R.font.robotomono_regular, FontWeight.Normal)
|
||||||
|
)
|
||||||
|
|
||||||
|
val quicksandFamily = FontFamily(
|
||||||
|
Font(R.font.quicksand_semibold, FontWeight.SemiBold)
|
||||||
|
)
|
||||||
|
|
||||||
|
val launcherTitleStyle = TextStyle(
|
||||||
|
fontFamily = quicksandFamily,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 32.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
)
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
package com.geode.launcher.updater
|
package com.geode.launcher.updater
|
||||||
|
|
||||||
import com.geode.launcher.utils.LaunchUtils
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
import kotlinx.datetime.Instant
|
import kotlin.time.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Asset(
|
data class Asset @OptIn(ExperimentalTime::class) constructor(
|
||||||
val url: String,
|
val url: String,
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
@@ -16,7 +17,7 @@ data class Asset(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Release(
|
data class Release @OptIn(ExperimentalTime::class) constructor(
|
||||||
val url: String,
|
val url: String,
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val targetCommitish: String,
|
val targetCommitish: String,
|
||||||
@@ -30,7 +31,7 @@ data class Release(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LoaderVersion(
|
data class LoaderVersion @OptIn(ExperimentalTime::class) constructor(
|
||||||
val tag: String,
|
val tag: String,
|
||||||
val version: String,
|
val version: String,
|
||||||
val createdAt: Instant,
|
val createdAt: Instant,
|
||||||
@@ -66,6 +67,7 @@ class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadab
|
|||||||
return release.tagName
|
return release.tagName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
override fun getDescriptor(): Long {
|
override fun getDescriptor(): Long {
|
||||||
return release.createdAt.epochSeconds
|
return release.createdAt.epochSeconds
|
||||||
}
|
}
|
||||||
@@ -95,6 +97,7 @@ class DownloadableLauncherRelease(val release: Release) : Downloadable() {
|
|||||||
return release.tagName
|
return release.tagName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
override fun getDescriptor(): Long {
|
override fun getDescriptor(): Long {
|
||||||
return release.createdAt.epochSeconds
|
return release.createdAt.epochSeconds
|
||||||
}
|
}
|
||||||
@@ -132,6 +135,7 @@ class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadab
|
|||||||
return version.tag
|
return version.tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
override fun getDescriptor(): Long {
|
override fun getDescriptor(): Long {
|
||||||
return version.createdAt.epochSeconds
|
return version.createdAt.epochSeconds
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,22 +122,12 @@ class ReleaseManager private constructor(
|
|||||||
val gameVersion = GamePackageUtils.getGameVersionCode(applicationContext.packageManager)
|
val gameVersion = GamePackageUtils.getGameVersionCode(applicationContext.packageManager)
|
||||||
val preferences = PreferenceUtils.get(applicationContext)
|
val preferences = PreferenceUtils.get(applicationContext)
|
||||||
|
|
||||||
if (preferences.getBoolean(PreferenceUtils.Key.USE_INDEX_API)) {
|
val targetTag = preferences.getInt(PreferenceUtils.Key.RELEASE_CHANNEL_TAG)
|
||||||
val targetTag = preferences.getInt(PreferenceUtils.Key.RELEASE_CHANNEL_TAG)
|
|
||||||
|
|
||||||
return when (targetTag) {
|
return when (targetTag) {
|
||||||
2 -> releaseRepository.getReleaseByTag("nightly")
|
2 -> releaseRepository.getReleaseByTag("nightly")
|
||||||
1 -> releaseRepository.getLatestIndexRelease(gameVersion, true)
|
1 -> releaseRepository.getLatestIndexRelease(gameVersion, true)
|
||||||
else -> releaseRepository.getLatestIndexRelease(gameVersion, false)
|
else -> releaseRepository.getLatestIndexRelease(gameVersion, false)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val targetTag = getBestReleaseForGameVersion(gameVersion) ?: return null
|
|
||||||
|
|
||||||
return when (targetTag) {
|
|
||||||
TAG_LATEST -> releaseRepository.getLatestGeodeRelease()
|
|
||||||
TAG_BETA -> releaseRepository.getLatestGeodePreRelease()
|
|
||||||
else -> releaseRepository.getReleaseByTag(targetTag)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +171,10 @@ class ReleaseManager private constructor(
|
|||||||
// set an initial download size
|
// set an initial download size
|
||||||
_uiState.value = ReleaseManagerState.InDownload(0, releaseAsset.size)
|
_uiState.value = ReleaseManagerState.InDownload(0, releaseAsset.size)
|
||||||
|
|
||||||
|
val outputFile = getTempFile()
|
||||||
|
// clone the file instance as renameTo may move the original file
|
||||||
|
val tempFilePath = outputFile.path
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val fileStream = DownloadUtils.downloadStream(
|
val fileStream = DownloadUtils.downloadStream(
|
||||||
httpClient,
|
httpClient,
|
||||||
@@ -189,23 +183,30 @@ class ReleaseManager private constructor(
|
|||||||
_uiState.value = ReleaseManagerState.InDownload(progress, outOf)
|
_uiState.value = ReleaseManagerState.InDownload(progress, outOf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
outputFile.parentFile?.mkdirs()
|
||||||
|
|
||||||
val geodeFile = getGeodeOutputPath()
|
val geodeFile = getGeodeOutputPath()
|
||||||
|
|
||||||
|
DownloadUtils.extractFileFromZipStream(
|
||||||
|
fileStream,
|
||||||
|
outputFile.outputStream(),
|
||||||
|
geodeFile.name
|
||||||
|
)
|
||||||
|
|
||||||
// work around a permission issue from adb push
|
// work around a permission issue from adb push
|
||||||
if (geodeFile.exists()) {
|
if (geodeFile.exists()) {
|
||||||
geodeFile.delete()
|
geodeFile.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
geodeFile.parentFile?.mkdirs()
|
outputFile.renameTo(geodeFile)
|
||||||
|
|
||||||
DownloadUtils.extractFileFromZipStream(
|
|
||||||
fileStream,
|
|
||||||
geodeFile.outputStream(),
|
|
||||||
geodeFile.name
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
sendError(e)
|
sendError(e)
|
||||||
return
|
return
|
||||||
|
} finally {
|
||||||
|
val tempFileClone = File(tempFilePath)
|
||||||
|
try {
|
||||||
|
if (tempFileClone.exists()) tempFileClone.delete()
|
||||||
|
} catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadLauncherUpdateIfNecessary()
|
downloadLauncherUpdateIfNecessary()
|
||||||
@@ -325,6 +326,15 @@ class ReleaseManager private constructor(
|
|||||||
return File(geodeDirectory, geodeName)
|
return File(geodeDirectory, geodeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getTempFile(): File {
|
||||||
|
val geodeName = LaunchUtils.geodeFilename
|
||||||
|
val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext)
|
||||||
|
|
||||||
|
val tempFile = File.createTempFile(geodeName, null, geodeDirectory)
|
||||||
|
|
||||||
|
return tempFile
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels the current update job.
|
* Cancels the current update job.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.geode.launcher.updater
|
package com.geode.launcher.updater
|
||||||
|
|
||||||
import com.geode.launcher.utils.DownloadUtils.executeCoroutine
|
import com.geode.launcher.utils.DownloadUtils.executeCoroutine
|
||||||
import kotlinx.datetime.Clock
|
import kotlin.time.Clock
|
||||||
import kotlinx.datetime.DateTimeUnit
|
import kotlinx.datetime.DateTimeUnit
|
||||||
import kotlinx.datetime.Instant
|
import kotlin.time.Instant
|
||||||
import kotlinx.datetime.until
|
import kotlinx.datetime.until
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -14,6 +14,7 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
|
||||||
class ReleaseRepository(private val httpClient: OkHttpClient) {
|
class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -49,7 +50,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
|||||||
return getReleaseByUrl(url)?.let(::DownloadableGitHubLoaderRelease)
|
return getReleaseByUrl(url)?.let(::DownloadableGitHubLoaderRelease)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class)
|
||||||
suspend fun getLatestGeodePreRelease(): DownloadableGitHubLoaderRelease? {
|
suspend fun getLatestGeodePreRelease(): DownloadableGitHubLoaderRelease? {
|
||||||
val releasesUrl = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases?per_page=2"
|
val releasesUrl = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases?per_page=2"
|
||||||
val url = URL(releasesUrl)
|
val url = URL(releasesUrl)
|
||||||
@@ -104,6 +105,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
private fun generateRateLimitMessage(resetTime: Instant): String {
|
private fun generateRateLimitMessage(resetTime: Instant): String {
|
||||||
val currentTime = Clock.System.now()
|
val currentTime = Clock.System.now()
|
||||||
val resetDelay = currentTime.until(resetTime, DateTimeUnit.SECOND)
|
val resetDelay = currentTime.until(resetTime, DateTimeUnit.SECOND)
|
||||||
@@ -113,7 +115,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
|||||||
return "api ratelimit reached, try again in ${formattedWait}"
|
return "api ratelimit reached, try again in ${formattedWait}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class)
|
||||||
private suspend fun getReleaseByUrl(url: URL): Release? {
|
private suspend fun getReleaseByUrl(url: URL): Release? {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ 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
|
||||||
@@ -16,6 +19,7 @@ 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
|
||||||
@@ -476,6 +480,10 @@ object GeodeUtils {
|
|||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun vibrate(ms: Long) {
|
fun vibrate(ms: Long) {
|
||||||
|
if (ms == 0L) {
|
||||||
|
getVibrator()?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
getVibrator()?.vibrate(VibrationEffect.createOneShot(ms, VibrationEffect.DEFAULT_AMPLITUDE))
|
getVibrator()?.vibrate(VibrationEffect.createOneShot(ms, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||||
} else {
|
} else {
|
||||||
@@ -542,6 +550,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"
|
||||||
|
|
||||||
private var capabilityListener: WeakReference<CapabilityListener?> = WeakReference(null)
|
private var capabilityListener: WeakReference<CapabilityListener?> = WeakReference(null)
|
||||||
|
|
||||||
@@ -571,6 +580,188 @@ 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)
|
||||||
@@ -582,4 +773,18 @@ object GeodeUtils {
|
|||||||
* @see reportPlatformCapability
|
* @see reportPlatformCapability
|
||||||
*/
|
*/
|
||||||
external fun setNextInputTimestamp(timestamp: Long)
|
external fun setNextInputTimestamp(timestamp: Long)
|
||||||
|
|
||||||
|
const val TOUCH_TYPE_BEGIN = 0
|
||||||
|
const val TOUCH_TYPE_MOVE = 1
|
||||||
|
const val TOUCH_TYPE_END = 2
|
||||||
|
const val TOUCH_TYPE_CANCEL = 3
|
||||||
|
|
||||||
|
external fun internalKeyEvent(timestamp: Long, deviceId: Int, eventSource: Int, keyCode: Int, modifiers: Int, isDown: Boolean, repeatCount: Int)
|
||||||
|
external fun internalTouchEvent(timestamp: Long, deviceId: Int, eventSource: Int, eventType: Int, ids: IntArray, xs: FloatArray, ys: FloatArray)
|
||||||
|
external fun internalScrollEvent(timestamp: Long, deviceId: Int, eventSource: Int, scrollX: Float, scrollY: Float)
|
||||||
|
external fun internalJoystickEvent(timestamp: Long, deviceId: Int, eventSource: Int, leftX: FloatArray, leftY: FloatArray, rightX: FloatArray, rightY: FloatArray, hatX: FloatArray, hatY: FloatArray, leftTrigger: FloatArray, rightTrigger: FloatArray)
|
||||||
|
|
||||||
|
external fun inputDeviceAdded(deviceId: Int)
|
||||||
|
external fun inputDeviceChanged(deviceId: Int)
|
||||||
|
external fun inputDeviceRemoved(deviceId: Int)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package com.geode.launcher.utils
|
||||||
|
|
||||||
|
import android.graphics.drawable.AdaptiveIconDrawable
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
|
import com.geode.launcher.R
|
||||||
|
|
||||||
|
// https://gist.github.com/tkuenneth/ddf598663f041dc79960cda503d14448?permalink_comment_id=4660486#gistcomment-4660486
|
||||||
|
@Composable
|
||||||
|
fun adaptiveIconPainterResource(@DrawableRes id: Int): Painter {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val res = LocalResources.current
|
||||||
|
val theme = context.theme
|
||||||
|
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val adaptiveIcon = remember(id) {
|
||||||
|
ResourcesCompat.getDrawable(res, id, theme) as? AdaptiveIconDrawable
|
||||||
|
}
|
||||||
|
if (adaptiveIcon != null) {
|
||||||
|
remember(id) {
|
||||||
|
BitmapPainter(adaptiveIcon.toBitmap().asImageBitmap())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
painterResource(id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
painterResource(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ApplicationIcon {
|
||||||
|
DEFAULT,
|
||||||
|
GEODE,
|
||||||
|
PRIDE,
|
||||||
|
TRANS;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromId(id: String) = when (id) {
|
||||||
|
"default" -> ApplicationIcon.DEFAULT
|
||||||
|
"geode" -> ApplicationIcon.GEODE
|
||||||
|
"pride" -> ApplicationIcon.PRIDE
|
||||||
|
"trans" -> ApplicationIcon.TRANS
|
||||||
|
else -> ApplicationIcon.DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toId() = when (this) {
|
||||||
|
ApplicationIcon.PRIDE -> "pride"
|
||||||
|
ApplicationIcon.GEODE -> "geode"
|
||||||
|
ApplicationIcon.DEFAULT -> "default"
|
||||||
|
ApplicationIcon.TRANS -> "trans"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ApplicationIconDetails(
|
||||||
|
val id: String,
|
||||||
|
val component: String,
|
||||||
|
@param:DrawableRes val iconId: Int,
|
||||||
|
@param:StringRes val nameId: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
object IconUtils {
|
||||||
|
fun getIconDetails(id: ApplicationIcon) = when (id) {
|
||||||
|
ApplicationIcon.DEFAULT -> ApplicationIconDetails(
|
||||||
|
id = id.toId(),
|
||||||
|
component = "MainActivity",
|
||||||
|
iconId = R.mipmap.ic_launcher,
|
||||||
|
nameId = R.string.application_icon_default
|
||||||
|
)
|
||||||
|
ApplicationIcon.GEODE -> ApplicationIconDetails(
|
||||||
|
id = id.toId(),
|
||||||
|
component = "MainActivityGeode",
|
||||||
|
iconId = R.mipmap.ic_launcher_geode,
|
||||||
|
nameId = R.string.application_icon_geode
|
||||||
|
)
|
||||||
|
ApplicationIcon.PRIDE -> ApplicationIconDetails(
|
||||||
|
id = id.toId(),
|
||||||
|
component = "MainActivityPride",
|
||||||
|
iconId = R.mipmap.ic_launcher_pride,
|
||||||
|
nameId = R.string.application_icon_pride
|
||||||
|
)
|
||||||
|
ApplicationIcon.TRANS -> ApplicationIconDetails(
|
||||||
|
id = id.toId(),
|
||||||
|
component = "MainActivityTrans",
|
||||||
|
iconId = R.mipmap.ic_launcher_trans,
|
||||||
|
nameId = R.string.application_icon_trans
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,9 +73,9 @@ object LaunchUtils {
|
|||||||
return File(getBaseDirectory(context), "save")
|
return File(getBaseDirectory(context), "save")
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val CRASH_INDICATOR_NAME = "lastSessionDidCrash"
|
const val CRASH_INDICATOR_NAME = "lastSessionDidCrash"
|
||||||
|
|
||||||
private fun getCrashDirectory(context: Context): File {
|
fun getCrashDirectory(context: Context): File {
|
||||||
val base = getBaseDirectory(context)
|
val base = getBaseDirectory(context)
|
||||||
return File(base, "game/geode/crashlogs/")
|
return File(base, "game/geode/crashlogs/")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,14 +148,14 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
SCREEN_ZOOM,
|
SCREEN_ZOOM,
|
||||||
SCREEN_ZOOM_FIT,
|
SCREEN_ZOOM_FIT,
|
||||||
LIMIT_FRAME_RATE,
|
LIMIT_FRAME_RATE,
|
||||||
USE_INDEX_API,
|
DISABLE_USER_THEME,
|
||||||
|
SELECTED_ICON
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
||||||
return when (key) {
|
return when (key) {
|
||||||
Key.UPDATE_AUTOMATICALLY, Key.FORCE_HRR -> true
|
Key.UPDATE_AUTOMATICALLY, Key.FORCE_HRR -> true
|
||||||
Key.DEVELOPER_MODE -> BuildConfig.DEBUG
|
Key.DEVELOPER_MODE -> BuildConfig.DEBUG
|
||||||
Key.USE_INDEX_API -> true
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,6 +169,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
|
|
||||||
private fun defaultValueForStringKey(key: Key) = when (key) {
|
private fun defaultValueForStringKey(key: Key) = when (key) {
|
||||||
Key.CUSTOM_ASPECT_RATIO -> "16_9"
|
Key.CUSTOM_ASPECT_RATIO -> "16_9"
|
||||||
|
Key.SELECTED_ICON -> "default"
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +198,8 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
Key.SCREEN_ZOOM -> "PreferenceScreenZoom"
|
Key.SCREEN_ZOOM -> "PreferenceScreenZoom"
|
||||||
Key.SCREEN_ZOOM_FIT -> "PreferenceScreenZoomFit"
|
Key.SCREEN_ZOOM_FIT -> "PreferenceScreenZoomFit"
|
||||||
Key.LIMIT_FRAME_RATE -> "PreferenceLimitFrameRate"
|
Key.LIMIT_FRAME_RATE -> "PreferenceLimitFrameRate"
|
||||||
Key.USE_INDEX_API -> "PreferenceUseIndexApi"
|
Key.DISABLE_USER_THEME -> "PreferenceDisableUserTheme"
|
||||||
|
Key.SELECTED_ICON -> "PreferenceSelectedIcon"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
package org.cocos2dx.lib
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import androidx.appcompat.widget.AppCompatEditText
|
|
||||||
|
|
||||||
class Cocos2dxEditText(context: Context) : AppCompatEditText(context) {
|
|
||||||
var cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView? = null
|
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean {
|
|
||||||
super.onKeyDown(keyCode, keyEvent)
|
|
||||||
if (keyCode != KeyEvent.KEYCODE_BACK) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
cocos2dxGLSurfaceView?.requestFocus()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,19 +2,23 @@ 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
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
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
|
||||||
@@ -56,13 +60,14 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
|
|
||||||
var sendTimestampEvents = false
|
var sendTimestampEvents = false
|
||||||
var manualBackEvents = false
|
var manualBackEvents = false
|
||||||
|
var controllerEventsEnabled = false
|
||||||
|
private set
|
||||||
|
|
||||||
var cocos2dxEditText: Cocos2dxEditText? = null
|
var cocos2dxEditText: AppCompatEditText? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
|
|
||||||
field?.setOnEditorActionListener(cocos2dxTextInputWrapper)
|
field?.setOnEditorActionListener(cocos2dxTextInputWrapper)
|
||||||
field?.cocos2dxGLSurfaceView = this
|
|
||||||
requestFocus()
|
requestFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +155,11 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
queueEvent { cocos2dxRenderer.handleOnResume() }
|
queueEvent { cocos2dxRenderer.handleOnResume() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sendNextEventTimestamp(timestamp: Long) {
|
||||||
|
if (sendTimestampEvents)
|
||||||
|
GeodeUtils.setNextInputTimestamp(timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onTouchEvent(motionEvent: MotionEvent): Boolean {
|
override fun onTouchEvent(motionEvent: MotionEvent): Boolean {
|
||||||
val pointerNumber = motionEvent.pointerCount
|
val pointerNumber = motionEvent.pointerCount
|
||||||
val ids = IntArray(pointerNumber)
|
val ids = IntArray(pointerNumber)
|
||||||
@@ -169,10 +179,16 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
val xDown = xs[0]
|
val xDown = xs[0]
|
||||||
val f = ys[0]
|
val f = ys[0]
|
||||||
queueEvent {
|
queueEvent {
|
||||||
if (sendTimestampEvents)
|
sendNextEventTimestamp(timestamp)
|
||||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
|
||||||
|
|
||||||
cocos2dxRenderer.handleActionDown(idDown, xDown, f)
|
if (controllerEventsEnabled)
|
||||||
|
GeodeUtils.internalTouchEvent(
|
||||||
|
timestamp, motionEvent.deviceId, motionEvent.source,
|
||||||
|
// slice here to convert to single element array
|
||||||
|
GeodeUtils.TOUCH_TYPE_BEGIN, ids.sliceArray(0..0), xs.sliceArray(0..0), ys.sliceArray(0..0)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
cocos2dxRenderer.handleActionDown(idDown, xDown, f)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -181,28 +197,43 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
val f2 = xs[0]
|
val f2 = xs[0]
|
||||||
val f3 = ys[0]
|
val f3 = ys[0]
|
||||||
queueEvent {
|
queueEvent {
|
||||||
if (sendTimestampEvents)
|
sendNextEventTimestamp(timestamp)
|
||||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
|
||||||
|
|
||||||
cocos2dxRenderer.handleActionUp(idUp, f2, f3)
|
if (controllerEventsEnabled)
|
||||||
|
GeodeUtils.internalTouchEvent(
|
||||||
|
timestamp, motionEvent.deviceId, motionEvent.source,
|
||||||
|
GeodeUtils.TOUCH_TYPE_END, ids.sliceArray(0..0), xs.sliceArray(0..0), ys.sliceArray(0..0)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
cocos2dxRenderer.handleActionUp(idUp, f2, f3)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_MOVE -> {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
queueEvent {
|
queueEvent {
|
||||||
if (sendTimestampEvents)
|
sendNextEventTimestamp(timestamp)
|
||||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
|
||||||
|
|
||||||
cocos2dxRenderer.handleActionMove(ids, xs, ys)
|
if (controllerEventsEnabled)
|
||||||
|
GeodeUtils.internalTouchEvent(
|
||||||
|
timestamp, motionEvent.deviceId, motionEvent.source,
|
||||||
|
GeodeUtils.TOUCH_TYPE_MOVE, ids, xs, ys
|
||||||
|
)
|
||||||
|
else
|
||||||
|
cocos2dxRenderer.handleActionMove(ids, xs, ys)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_CANCEL -> {
|
MotionEvent.ACTION_CANCEL -> {
|
||||||
queueEvent {
|
queueEvent {
|
||||||
if (sendTimestampEvents)
|
sendNextEventTimestamp(timestamp)
|
||||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
|
||||||
|
|
||||||
cocos2dxRenderer.handleActionCancel(ids, xs, ys)
|
if (controllerEventsEnabled)
|
||||||
|
GeodeUtils.internalTouchEvent(
|
||||||
|
timestamp, motionEvent.deviceId, motionEvent.source,
|
||||||
|
GeodeUtils.TOUCH_TYPE_CANCEL, ids, xs, ys
|
||||||
|
)
|
||||||
|
else
|
||||||
|
cocos2dxRenderer.handleActionCancel(ids, xs, ys)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -212,10 +243,15 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
val xPointerDown = motionEvent.getX(indexPointerDown)
|
val xPointerDown = motionEvent.getX(indexPointerDown)
|
||||||
val y = motionEvent.getY(indexPointerDown)
|
val y = motionEvent.getY(indexPointerDown)
|
||||||
queueEvent {
|
queueEvent {
|
||||||
if (sendTimestampEvents)
|
sendNextEventTimestamp(timestamp)
|
||||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
|
||||||
|
|
||||||
cocos2dxRenderer.handleActionDown(idPointerDown, xPointerDown, y)
|
if (controllerEventsEnabled)
|
||||||
|
GeodeUtils.internalTouchEvent(
|
||||||
|
timestamp, motionEvent.deviceId, motionEvent.source,
|
||||||
|
GeodeUtils.TOUCH_TYPE_BEGIN, intArrayOf(idPointerDown), floatArrayOf(xPointerDown), floatArrayOf(y)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
cocos2dxRenderer.handleActionDown(idPointerDown, xPointerDown, y)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -225,10 +261,15 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
val xPointerUp = motionEvent.getX(indexPointUp)
|
val xPointerUp = motionEvent.getX(indexPointUp)
|
||||||
val y2 = motionEvent.getY(indexPointUp)
|
val y2 = motionEvent.getY(indexPointUp)
|
||||||
queueEvent {
|
queueEvent {
|
||||||
if (sendTimestampEvents)
|
sendNextEventTimestamp(timestamp)
|
||||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
|
||||||
|
|
||||||
cocos2dxRenderer.handleActionUp(idPointerUp, xPointerUp, y2)
|
if (controllerEventsEnabled)
|
||||||
|
GeodeUtils.internalTouchEvent(
|
||||||
|
timestamp, motionEvent.deviceId, motionEvent.source,
|
||||||
|
GeodeUtils.TOUCH_TYPE_END, intArrayOf(idPointerUp), floatArrayOf(xPointerUp), floatArrayOf(y2)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
cocos2dxRenderer.handleActionUp(idPointerUp, xPointerUp, y2)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -261,27 +302,50 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sendKeyBack() {
|
fun sendKeyBack() {
|
||||||
|
if (cocos2dxEditText?.isFocused == true) {
|
||||||
|
requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
if (BaseRobTopActivity.blockBackButton) {
|
if (BaseRobTopActivity.blockBackButton) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useKeyboardEvents) {
|
val currentTime = System.nanoTime()
|
||||||
queueEvent {
|
queueEvent {
|
||||||
cocos2dxRenderer.handleKeyDown(KeyEvent.KEYCODE_BACK)
|
sendNextEventTimestamp(currentTime)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val currentTime = System.nanoTime()
|
|
||||||
queueEvent {
|
|
||||||
if (sendTimestampEvents)
|
|
||||||
GeodeUtils.setNextInputTimestamp(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) {
|
if (!useKeyboardEvents && !controllerEventsEnabled) {
|
||||||
return legacyKeyDown(keyCode, event)
|
return legacyKeyDown(keyCode, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,10 +360,15 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
queueEvent {
|
queueEvent {
|
||||||
if (sendTimestampEvents)
|
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
|
||||||
GeodeUtils.setNextInputTimestamp(event.eventTime * MS_TO_NS)
|
|
||||||
|
|
||||||
GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0)
|
if (useKeyboardEvents) {
|
||||||
|
GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controllerEventsEnabled) {
|
||||||
|
GeodeUtils.internalKeyEvent(event.eventTime * MS_TO_NS, event.deviceId, event.source, event.keyCode, event.modifiers, true, event.repeatCount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -307,7 +376,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
if (!useKeyboardEvents) {
|
if (!useKeyboardEvents && !controllerEventsEnabled) {
|
||||||
return super.onKeyUp(keyCode, event)
|
return super.onKeyUp(keyCode, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,10 +390,15 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
queueEvent {
|
queueEvent {
|
||||||
if (sendTimestampEvents)
|
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
|
||||||
GeodeUtils.setNextInputTimestamp(event.eventTime * MS_TO_NS)
|
|
||||||
|
|
||||||
GeodeUtils.nativeKeyUp(keyCode, event.modifiers)
|
if (useKeyboardEvents) {
|
||||||
|
GeodeUtils.nativeKeyUp(keyCode, event.modifiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controllerEventsEnabled) {
|
||||||
|
GeodeUtils.internalKeyEvent(event.eventTime * MS_TO_NS, event.deviceId, event.source, event.keyCode, event.modifiers, false, event.repeatCount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
@@ -332,8 +406,67 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// i'm lazy.
|
||||||
|
// https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input
|
||||||
|
private fun getCenteredAxis(
|
||||||
|
event: MotionEvent,
|
||||||
|
device: InputDevice,
|
||||||
|
axis: Int,
|
||||||
|
historyPos: Int
|
||||||
|
): Float {
|
||||||
|
device.getMotionRange(axis, event.source)?.apply {
|
||||||
|
val value: Float = if (historyPos < 0) {
|
||||||
|
event.getAxisValue(axis)
|
||||||
|
} else {
|
||||||
|
event.getHistoricalAxisValue(axis, historyPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abs(value) > flat) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0.0f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getJoystickMotion(event: MotionEvent, axisHorizontal: Int, axisVertical: Int): Pair<FloatArray, FloatArray> {
|
||||||
|
val positionChangesX = ArrayList<Float>()
|
||||||
|
val positionChangesY = ArrayList<Float>()
|
||||||
|
|
||||||
|
val inputDevice = event.device
|
||||||
|
|
||||||
|
(0 until event.historySize).forEach { i ->
|
||||||
|
val x = getCenteredAxis(event, inputDevice, axisHorizontal, i)
|
||||||
|
val y = getCenteredAxis(event, inputDevice, axisVertical, i)
|
||||||
|
positionChangesX.add(x)
|
||||||
|
positionChangesY.add(y)
|
||||||
|
}
|
||||||
|
|
||||||
|
val x = getCenteredAxis(event, inputDevice, axisHorizontal, -1)
|
||||||
|
val y = getCenteredAxis(event, inputDevice, axisVertical, -1)
|
||||||
|
positionChangesX.add(x)
|
||||||
|
positionChangesY.add(y)
|
||||||
|
|
||||||
|
return Pair(positionChangesX.toFloatArray(), positionChangesY.toFloatArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getJoystickMotion(event: MotionEvent, axis: Int): FloatArray {
|
||||||
|
val positionChanges = ArrayList<Float>()
|
||||||
|
|
||||||
|
val inputDevice = event.device
|
||||||
|
|
||||||
|
(0 until event.historySize).forEach { i ->
|
||||||
|
val x = getCenteredAxis(event, inputDevice, axis, i)
|
||||||
|
positionChanges.add(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
val x = getCenteredAxis(event, inputDevice, axis, -1)
|
||||||
|
positionChanges.add(x)
|
||||||
|
|
||||||
|
return positionChanges.toFloatArray()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
|
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
|
||||||
if (!useKeyboardEvents) {
|
if (!useKeyboardEvents && !controllerEventsEnabled) {
|
||||||
return super.onGenericMotionEvent(event)
|
return super.onGenericMotionEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,10 +477,40 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
|||||||
event.eventTimeNanos else event.eventTime * MS_TO_NS
|
event.eventTimeNanos else event.eventTime * MS_TO_NS
|
||||||
|
|
||||||
queueEvent {
|
queueEvent {
|
||||||
if (sendTimestampEvents)
|
sendNextEventTimestamp(timestamp)
|
||||||
GeodeUtils.setNextInputTimestamp(timestamp)
|
|
||||||
|
|
||||||
GeodeUtils.nativeActionScroll(scrollX, scrollY)
|
if (useKeyboardEvents) {
|
||||||
|
GeodeUtils.nativeActionScroll(scrollX, scrollY)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controllerEventsEnabled) {
|
||||||
|
GeodeUtils.internalScrollEvent(timestamp, event.deviceId, event.source, scrollX, scrollY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controllerEventsEnabled && event?.action == MotionEvent.ACTION_MOVE && event.source and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK) {
|
||||||
|
val (motionLeftX, motionLeftY) = getJoystickMotion(event, MotionEvent.AXIS_X, MotionEvent.AXIS_Y)
|
||||||
|
val (motionRightX, motionRightY) = getJoystickMotion(event, MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ)
|
||||||
|
val (motionDPadX, motionDPadY) = getJoystickMotion(event, MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y)
|
||||||
|
|
||||||
|
val motionLTrigger = getJoystickMotion(event, MotionEvent.AXIS_LTRIGGER)
|
||||||
|
val motionRTrigger = getJoystickMotion(event, MotionEvent.AXIS_RTRIGGER)
|
||||||
|
|
||||||
|
// i'm sure this is great for performance
|
||||||
|
val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
event.eventTimeNanos else event.eventTime * MS_TO_NS
|
||||||
|
|
||||||
|
queueEvent {
|
||||||
|
GeodeUtils.internalJoystickEvent(
|
||||||
|
timestamp, event.deviceId, event.source,
|
||||||
|
motionLeftX, motionLeftY,
|
||||||
|
motionRightX, motionRightY,
|
||||||
|
motionDPadX, motionDPadY,
|
||||||
|
motionLTrigger, motionRTrigger
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -372,4 +535,40 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -52,12 +52,10 @@ class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurf
|
|||||||
cocos2dxGLSurfaceView.deleteBackward()
|
cocos2dxGLSurfaceView.deleteBackward()
|
||||||
}
|
}
|
||||||
var text = textView.text.toString()
|
var text = textView.text.toString()
|
||||||
if (text.compareTo("") == 0) {
|
if (!text.endsWith('\n')) {
|
||||||
text = "\n"
|
text += '\n'
|
||||||
}
|
|
||||||
if (10 != text[text.length - 1].code) {
|
|
||||||
text += 10.toChar()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cocos2dxGLSurfaceView.insertText(text)
|
cocos2dxGLSurfaceView.insertText(text)
|
||||||
}
|
}
|
||||||
if (actionID != 6) {
|
if (actionID != 6) {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<group android:scaleX="0.6689189"
|
||||||
|
android:scaleY="0.6689189"
|
||||||
|
android:translateX="169.51352"
|
||||||
|
android:translateY="182.1982">
|
||||||
|
<path
|
||||||
|
android:pathData="M553.7,189.9L536.9,177.1C522.2,165.8 501.8,165.8 487.1,177.1L207.6,390.5C194,400.9 188.3,418.8 193.3,435.2L300.9,783.2C306.2,800.3 322.1,812.1 340.1,812.1H683.9C701.9,812.1 717.8,800.3 723.1,783.2L830.7,435.2C835.7,418.8 830,400.9 816.4,390.5L792,371.9L762,466.3L680,733C676.5,744.4 666,752.1 654.2,752.1H368C356.1,752.1 345.7,744.4 342.2,733L252.8,442.3C249.5,431.5 253.2,419.8 262.1,413L494.6,234.5C495.6,233.7 496.6,233.1 497.6,232.5L553.7,189.9Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.25"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M500.6,320.6C508.5,314.5 519.5,314.5 527.4,320.6L686,442.2C693.3,447.8 696.3,457.3 693.6,466.1L632.6,664.4C629.7,673.6 621.2,679.9 611.6,679.9H416.4C406.8,679.9 398.3,673.6 395.4,664.4L385.4,632H439.4C440,632.1 440.7,632.2 441.4,632.2H583.2C588,632.2 592.3,629 593.7,624.4L638,477.5C639.3,473.2 637.8,468.5 634.3,465.7L519.1,375.6C515.1,372.5 509.5,372.5 505.5,375.6L399.1,458.8L344.5,499.1L334.4,466.1C331.7,457.3 334.7,447.8 342,442.2L500.6,320.6Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.25"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M557.1,165.4L536.9,150C522.2,138.8 501.8,138.8 487.1,150L180.9,383.8C167.3,394.2 161.6,412.1 166.7,428.5L284.4,809.4C289.7,826.6 305.6,838.3 323.6,838.3H700.4C718.4,838.3 734.3,826.6 739.6,809.4L857.3,428.5C862.4,412.1 856.7,394.2 843.1,383.8L814.8,362.3L782.4,464.4L693.2,754.4C689.7,765.7 679.2,773.5 667.4,773.5H354.6C342.8,773.5 332.3,765.7 328.8,754.4L231.2,437C227.9,426.2 231.6,414.5 240.6,407.6L494.6,212.6C495.6,211.8 496.6,211.1 497.7,210.5L557.1,165.4Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#ffffff" />
|
||||||
|
<path
|
||||||
|
android:pathData="M500.6,284.3C508.5,278.2 519.5,278.2 527.4,284.3L716.8,429.5C724.1,435.1 727.1,444.6 724.4,453.4L651.6,689.9C648.8,699.2 640.3,705.5 630.6,705.5H397.4C387.7,705.5 379.2,699.2 376.4,689.9L363.9,649.5H425.7C426.4,649.6 427.1,649.7 427.9,649.7H596.1C601,649.7 605.2,646.5 606.6,641.9L659.1,468C660.5,463.6 659,458.9 655.4,456.1L518.8,349.3C514.8,346.2 509.2,346.2 505.2,349.3L379.9,447.3L316.2,494.3L303.6,453.4C300.9,444.6 303.9,435.1 311.2,429.5L500.6,284.3Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#ffffff" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h1024v1024h-1024z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h1024v1024h-1024z"
|
||||||
|
android:fillColor="#F4D48E"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
|
||||||
|
android:fillColor="#F4D48E"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
|
||||||
|
android:fillColor="#F4A47B"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
|
||||||
|
android:fillColor="#E27880"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
|
||||||
|
android:fillColor="#B9588F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
|
||||||
|
android:fillColor="#714A9A"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h1024v1024h-1024z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h1024v1024h-1024z"
|
||||||
|
android:fillColor="#CC7C7C"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
|
||||||
|
android:fillColor="#CC7C7C"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
|
||||||
|
android:fillColor="#C6B471"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
|
||||||
|
android:fillColor="#59AF7F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
|
||||||
|
android:fillColor="#5F8CAB"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
|
||||||
|
android:fillColor="#70558D"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h1024v1024h-1024z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h1024v1024h-1024z"
|
||||||
|
android:fillColor="#65B8C2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
|
||||||
|
android:fillColor="#65B8C2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
|
||||||
|
android:fillColor="#DEA5CD"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
|
||||||
|
android:fillColor="#DEE8EA"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
|
||||||
|
android:fillColor="#C485BD"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
|
||||||
|
android:fillColor="#259CB4"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L400,160L480,240L800,240Q833,240 856.5,263.5Q880,287 880,320L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720L800,720Q800,720 800,720Q800,720 800,720L800,320Q800,320 800,320Q800,320 800,320L447,320L367,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720ZM160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240L160,320L160,320Q160,320 160,320Q160,320 160,320L160,720Q160,720 160,720Q160,720 160,720Z"/>
|
||||||
|
</vector>
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_geode_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_monochrome_shadow"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_pride_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_monochrome_shadow"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_trans_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_monochrome_shadow"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<string name="launcher_logo_alt">geode logo</string>
|
<string name="launcher_logo_alt">geode logo</string>
|
||||||
<string name="launcher_launch_icon_alt">play icon</string>
|
<string name="launcher_launch_icon_alt">play icon</string>
|
||||||
<string name="launcher_download_icon_alt">download icon</string>
|
<string name="launcher_download_icon_alt">download icon</string>
|
||||||
<string name="launcher_download_game">Download Game</string>
|
<string name="launcher_download_game">Download game</string>
|
||||||
<string name="launcher_download_32bit">Download the 32-bit version of launcher.</string>
|
<string name="launcher_download_32bit">Download the 32-bit version of launcher.</string>
|
||||||
<string name="launcher_download_universal">Download the non 32-bit version of launcher.</string>
|
<string name="launcher_download_universal">Download the non 32-bit version of launcher.</string>
|
||||||
<string name="launcher_settings_icon_alt">settings icon</string>
|
<string name="launcher_settings_icon_alt">settings icon</string>
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
<string name="title_activity_settings">Settings</string>
|
<string name="title_activity_settings">Settings</string>
|
||||||
<string name="back_icon_alt">back</string>
|
<string name="back_icon_alt">back</string>
|
||||||
|
|
||||||
<string name="export_folder_tag">Launcher External Folder</string>
|
<string name="export_folder_tag">Launcher external folder</string>
|
||||||
<string name="text_copied">Copied to clipboard.</string>
|
<string name="text_copied">Copied to clipboard.</string>
|
||||||
|
|
||||||
<string name="preference_category_testing">General</string>
|
<string name="preference_category_testing">General</string>
|
||||||
@@ -129,6 +129,7 @@
|
|||||||
<string name="preferences_copy_external_button">Copy external files directory</string>
|
<string name="preferences_copy_external_button">Copy external files directory</string>
|
||||||
<string name="preferences_open_file_manager">Open file manager</string>
|
<string name="preferences_open_file_manager">Open file manager</string>
|
||||||
<string name="preferences_view_logs">View application logs</string>
|
<string name="preferences_view_logs">View application logs</string>
|
||||||
|
<string name="preferences_view_crashes">View crash dumps</string>
|
||||||
|
|
||||||
<string name="preference_theme_name">Theme</string>
|
<string name="preference_theme_name">Theme</string>
|
||||||
<string name="preference_theme_select">Select theme</string>
|
<string name="preference_theme_select">Select theme</string>
|
||||||
@@ -136,6 +137,8 @@
|
|||||||
<string name="preference_theme_light">Light</string>
|
<string name="preference_theme_light">Light</string>
|
||||||
<string name="preference_theme_dark">Dark</string>
|
<string name="preference_theme_dark">Dark</string>
|
||||||
<string name="preference_black_background_name">Black background in dark theme</string>
|
<string name="preference_black_background_name">Black background in dark theme</string>
|
||||||
|
<string name="preference_disable_styles">Disable custom styling</string>
|
||||||
|
<string name="preference_set_application_icon">Application icon</string>
|
||||||
<string name="preference_force_hrr">Force maximum refresh rate</string>
|
<string name="preference_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>
|
||||||
|
|
||||||
@@ -219,6 +222,7 @@
|
|||||||
<string name="preference_text_clear">Clear</string>
|
<string name="preference_text_clear">Clear</string>
|
||||||
|
|
||||||
<string name="preference_open_developer_options">Open developer options</string>
|
<string name="preference_open_developer_options">Open developer options</string>
|
||||||
|
<string name="preference_developer_options_dialog_title">Enable developer options?</string>
|
||||||
|
|
||||||
<string name="preference_profiles">Profiles</string>
|
<string name="preference_profiles">Profiles</string>
|
||||||
<string name="preference_profiles_create">Create profile</string>
|
<string name="preference_profiles_create">Create profile</string>
|
||||||
@@ -234,26 +238,42 @@
|
|||||||
<string name="preference_profiles_delete_undo_alt">Undo</string>
|
<string name="preference_profiles_delete_undo_alt">Undo</string>
|
||||||
|
|
||||||
<!-- ApplicationLogsActivity strings -->
|
<!-- ApplicationLogsActivity strings -->
|
||||||
<string name="title_activity_application_logs">Application Logs</string>
|
<string name="title_activity_application_logs">Application logs</string>
|
||||||
<string name="application_logs_no_logs">No logs available.</string>
|
<string name="application_logs_no_logs">No logs available.</string>
|
||||||
<string name="application_logs_delete">Clear All</string>
|
<string name="application_logs_delete">Clear all</string>
|
||||||
<string name="application_logs_share">Share</string>
|
<string name="application_logs_share">Share</string>
|
||||||
<string name="application_log_copy_label">Log Message</string>
|
<string name="application_log_copy_label">Log message</string>
|
||||||
<string name="application_logs_more">More</string>
|
<string name="application_logs_more">More</string>
|
||||||
<string name="application_logs_export">Save To</string>
|
<string name="application_logs_export">Save to</string>
|
||||||
<string name="application_logs_level">Set Log Level</string>
|
<string name="application_logs_level">Set log level</string>
|
||||||
<string name="application_logs_level_dialog_name">Set minimum log level</string>
|
<string name="application_logs_level_dialog_name">Set minimum log level</string>
|
||||||
<string name="application_logs_export_completed">Log export complete.</string>
|
<string name="application_logs_export_completed">Log export complete.</string>
|
||||||
<string name="application_logs_default_filename" translatable="false">launcher_logcat.txt</string>
|
<string name="application_logs_default_filename" translatable="false">launcher_logcat.txt</string>
|
||||||
<string name="application_logs_crash_only">Show Crash Buffer</string>
|
<string name="application_logs_crash_only">Show crash buffer</string>
|
||||||
<string name="application_logs_crash_only_enabled_alt">Enabled</string>
|
<string name="application_logs_crash_only_enabled_alt">Enabled</string>
|
||||||
|
|
||||||
<string name="title_activity_developer_settings">Developer Settings</string>
|
<!-- DeveloperSettingsActivity strings -->
|
||||||
|
<string name="title_activity_developer_settings">Developer options</string>
|
||||||
<string name="preference_developer_mode">Enable developer options</string>
|
<string name="preference_developer_mode">Enable developer options</string>
|
||||||
<string name="preference_developer_mode_about">Developer mode enables some options that may make the game unstable. Please do not modify them unless you know what you\'re doing!\nThese options may change behavior, or be removed entirely in future updates.</string>
|
<string name="preference_developer_mode_about">Developer mode allows changing options that may make the game unstable. Please do not modify them unless you know what you\'re doing!\nThese options may change behavior, or be removed entirely in future updates.</string>
|
||||||
<string name="preference_developer_update_notice">Enabling developer mode automatically disallows the updater from overwriting custom loader builds.</string>
|
<string name="preference_developer_update_notice">Enabling developer mode automatically disallows the updater from overwriting custom loader builds.</string>
|
||||||
|
|
||||||
<string name="preference_override_exceptions_name">Custom exceptions fix symbols</string>
|
<string name="preference_override_exceptions_name">Custom exceptions fix symbols</string>
|
||||||
<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>
|
||||||
|
|
||||||
|
<!-- CrashLogsActivity strings -->
|
||||||
|
<string name="title_activity_crash_logs">Crash dumps</string>
|
||||||
|
<string name="application_crashes_no_dumps">No crash dumps available.</string>
|
||||||
|
<string name="application_crashes_delete_one">Delete</string>
|
||||||
|
|
||||||
|
<!-- ApplicationIconActivity strings -->
|
||||||
|
<string name="title_activity_application_icon">Application icons</string>
|
||||||
|
<string name="application_icon_set_failed">Failed to change icon.</string>
|
||||||
|
<string name="application_icon_default">Default</string>
|
||||||
|
<string name="application_icon_geode">Geode</string>
|
||||||
|
<string name="application_icon_pride">Pride</string>
|
||||||
|
<string name="application_icon_trans">Transgender</string>
|
||||||
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
+6
-6
@@ -1,14 +1,14 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
val composeBOM by extra("2025.04.01")
|
val composeBOM by extra("2025.09.00")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "8.9.2" apply false
|
id("com.android.application") version "8.13.0" apply false
|
||||||
id("com.android.library") version "8.9.2" apply false
|
id("com.android.library") version "8.13.0" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.1.20" apply false
|
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.1.20" apply false
|
id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" apply false
|
||||||
id("org.jetbrains.kotlin.plugin.compose") version "2.1.20" apply false
|
id("org.jetbrains.kotlin.plugin.compose") version "2.2.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Delete>("clean") {
|
tasks.register<Delete>("clean") {
|
||||||
|
|||||||
Reference in new issue
Block a user