Compare commits

...
22 Commits
Author SHA1 Message Date
Chloe 269f2fbd02 some release download state fixes 2026-04-01 14:17:57 -07:00
Chloe 21c3374248 remove the other symbol too 2026-04-01 14:04:05 -07:00
Chloe 120f412986 remove symbol from remove list 2026-04-01 13:53:01 -07:00
Chloe 633543f363 1.7.5 2026-04-01 13:05:33 -07:00
Chloe d6b4157cdc fix icon size for animated logo parts 2026-04-01 13:00:32 -07:00
Chloe 482e3bc5d0 fix overflow 2026-04-01 12:58:23 -07:00
Chloe 15e24b566e minimize sapphire font
it was like 2mb for some reason
2026-04-01 03:24:29 -07:00
Chloe 0de077ec1d safe mode button was also broken lol 2026-04-01 03:12:47 -07:00
Chloe 655587e1ee use the proper logo 2026-04-01 03:08:12 -07:00
Chloe d3ad272ffc more sapphire changes 2026-04-01 02:41:23 -07:00
Chloe 79632c9844 fix return to main when application icon is in use 2026-04-01 01:50:01 -07:00
Chloe 8f5b1b4218 add april fools check to sapphire 2026-03-31 22:22:45 -07:00
Chloe 23ed997640 perform resources download within launcher 2026-03-31 22:14:04 -07:00
Chloe acaff15d4d release preparations 2026-03-31 21:22:42 -07:00
Chloe 669ea87be0 add sapphire icon 2026-03-31 21:05:57 -07:00
Chloe b668e5032b warn on below 2.208 2026-03-31 21:05:06 -07:00
Chloe 4508abc155 prompt user to view crash in error dialog 2026-03-21 23:16:16 -07:00
Chloe 38a994436f move geode logs button to settings 2026-03-21 23:13:41 -07:00
Chloe 5ab71d6c62 add share button to logs viewer 2026-03-21 23:00:38 -07:00
Chloe 3b88a32e8f they are named logs 2026-03-21 18:36:37 -07:00
Chloe 0ceecbbc72 optimize computeFileHash to avoid large allocation 2026-03-21 18:36:10 -07:00
Chloe 714f750f77 add support for text based crash dumps 2026-03-19 15:25:59 -07:00
29 changed files with 605 additions and 119 deletions

No files matched your search

+8 -8
View File
@@ -19,8 +19,8 @@ android {
applicationId = "com.geode.launcher" applicationId = "com.geode.launcher"
minSdk = 23 minSdk = 23
targetSdk = 36 targetSdk = 36
versionCode = 26 versionCode = 28
versionName = "1.7.3" versionName = "1.7.5"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true
@@ -78,7 +78,7 @@ android {
} }
externalNativeBuild { externalNativeBuild {
cmake { cmake {
version = "4.0.0+" version = "3.21.0+"
path = file("src/main/cpp/CMakeLists.txt") path = file("src/main/cpp/CMakeLists.txt")
} }
} }
@@ -88,18 +88,18 @@ android {
dependencies { dependencies {
implementation (platform("androidx.compose:compose-bom:$composeBOM")) implementation (platform("androidx.compose:compose-bom:$composeBOM"))
implementation ("androidx.core:core-ktx:1.17.0") implementation ("androidx.core:core-ktx:1.18.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.10.0") implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0") implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
implementation ("androidx.activity:activity-compose:1.12.4") implementation ("androidx.activity:activity-compose:1.13.0")
implementation ("androidx.activity:activity-ktx:1.12.4") implementation ("androidx.activity:activity-ktx:1.13.0")
implementation ("androidx.appcompat:appcompat:1.7.1") implementation ("androidx.appcompat:appcompat:1.7.1")
implementation ("androidx.documentfile:documentfile:1.1.0") implementation ("androidx.documentfile:documentfile:1.1.0")
implementation ("com.squareup.okio:okio:3.16.4") implementation ("com.squareup.okio:okio:3.17.0")
implementation ("com.squareup.okhttp3:okhttp:5.3.2") implementation ("com.squareup.okhttp3:okhttp:5.3.2")
implementation ("com.squareup.okhttp3:okhttp-coroutines:5.3.2") implementation ("com.squareup.okhttp3:okhttp-coroutines:5.3.2")
@@ -109,7 +109,7 @@ dependencies {
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.39.2") implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.39.2")
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.39.2") implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.39.2")
implementation ("androidx.browser:browser:1.9.0") implementation ("androidx.browser:browser:1.10.0")
debugImplementation ("androidx.compose.ui:ui-tooling") debugImplementation ("androidx.compose.ui:ui-tooling")
coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.1.5") coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.1.5")
} }
+21 -1
View File
@@ -103,7 +103,7 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".preferences.GeodeLogViewActivity" android:name=".preferences.TextViewActivity"
android:exported="false" android:exported="false"
android:theme="@style/Theme.GeodeLauncher"> android:theme="@style/Theme.GeodeLauncher">
</activity> </activity>
@@ -195,6 +195,26 @@
<data android:scheme="geode-launcher" android:host="main" /> <data android:scheme="geode-launcher" android:host="main" />
</intent-filter> </intent-filter>
</activity-alias> </activity-alias>
<activity-alias
android:name=".MainActivitySapphire"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_sapphire"
android:label="@string/application_icon_sapphire"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="geode-launcher" android:host="main" />
</intent-filter>
</activity-alias>
<activity-alias <activity-alias
android:name=".MainActivityGeode" android:name=".MainActivityGeode"
android:enabled="false" android:enabled="false"
+1 -1
View File
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 4.1.2) cmake_minimum_required(VERSION 3.21)
project(launcherfix) project(launcherfix)
+5 -4
View File
@@ -15,7 +15,7 @@ public:
}; };
// this is every function that i thought would be relevant // this is every function that i thought would be relevant
constexpr std::array<const std::string_view, 12> remove_symbols{ constexpr std::array<const std::string_view, 10> remove_symbols{
"__gxx_personality_v0", "__gxx_personality_v0",
"__cxa_throw", "__cxa_throw",
"__cxa_rethrow", "__cxa_rethrow",
@@ -25,9 +25,10 @@ constexpr std::array<const std::string_view, 12> remove_symbols{
"__cxa_guard_abort", "__cxa_guard_abort",
"__cxa_guard_acquire", "__cxa_guard_acquire",
"__cxa_guard_release", "__cxa_guard_release",
"__cxa_free_exception", "__cxa_free_exception"
"__emutls_get_address", // added in 2.208 // conflicts with fix within geode on some devices
"__cxa_thread_atexit" // "__emutls_get_address", // added in 2.208
// "__cxa_thread_atexit"
// android 10's libc doesn't have these symbols // android 10's libc doesn't have these symbols
// and the ndk is forbidden from exporting them!! so keep it for old mods... // and the ndk is forbidden from exporting them!! so keep it for old mods...
@@ -145,7 +145,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
returnMessage: String? = null, returnMessage: String? = null,
returnExtendedMessage: String? = null returnExtendedMessage: String? = null
) { ) {
val launchIntent = Intent(this, MainActivity::class.java).apply { val launchIntent = packageManager.getLaunchIntentForPackage(packageName)!!.apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
if (error != null && !returnMessage.isNullOrEmpty()) { if (error != null && !returnMessage.isNullOrEmpty()) {
@@ -7,6 +7,8 @@ import android.text.format.Formatter
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.LinearEasing
@@ -70,6 +72,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -82,11 +85,14 @@ import com.geode.launcher.ui.theme.Theme
import com.geode.launcher.ui.theme.Theme.DARK import com.geode.launcher.ui.theme.Theme.DARK
import com.geode.launcher.ui.theme.Theme.LIGHT import com.geode.launcher.ui.theme.Theme.LIGHT
import com.geode.launcher.ui.theme.launcherTitleStyle import com.geode.launcher.ui.theme.launcherTitleStyle
import com.geode.launcher.ui.theme.sapphireTitleStyle
import com.geode.launcher.updater.ReleaseManager import com.geode.launcher.updater.ReleaseManager
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.LaunchUtils import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
import com.geode.launcher.utils.checkIconDate
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.ConnectException import java.net.ConnectException
@@ -452,36 +458,76 @@ fun ExtraOptions(onSettings: () -> Unit) {
} }
} }
val lightOuterColorGradient = arrayOf( data class ColorPalette(
val outerColors: List<Pair<Float, Color>>,
val innerColors: List<Pair<Float, Color>>,
)
data class AnimatedIconSpec(
val lightPalette: ColorPalette,
val darkPalette: ColorPalette,
@param:DrawableRes val innerId: Int,
@param:DrawableRes val outerId: Int,
)
data class BrandPalette(
@param:DrawableRes val darkLogo: Int,
@param:DrawableRes val lightLogo: Int,
@param:StringRes val title: Int,
val titleFont: TextStyle,
val animatedIcon: AnimatedIconSpec? = null,
)
val geodeColorPalette = BrandPalette(
animatedIcon = AnimatedIconSpec(
lightPalette = ColorPalette(
outerColors = listOf(
0.0f to Color(0xfff5b11d), 0.0f to Color(0xfff5b11d),
0.10f to Color(0xfff0834d), 0.10f to Color(0xfff0834d),
0.24f to Color(0xffe85f6b), 0.24f to Color(0xffe85f6b),
0.49f to Color(0xffc9659e), 0.49f to Color(0xffc9659e),
1.0f to Color(0xffb9588f) 1.0f to Color(0xffb9588f)
) ),
innerColors = listOf(
val lightInnerColorGradient = arrayOf(
0.48f to Color(0xfff2ce00), 0.48f to Color(0xfff2ce00),
0.63f to Color(0xfff4b97e), 0.63f to Color(0xfff4b97e),
0.73f to Color(0xfff7b66a), 0.73f to Color(0xfff7b66a),
1.0f to Color(0xffeb8fac) 1.0f to Color(0xffeb8fac)
) )
),
val darkOuterColorGradient = arrayOf( darkPalette = ColorPalette(
outerColors = listOf(
0.0f to Color(0xfffffdff), 0.0f to Color(0xfffffdff),
0.08f to Color(0xffF2EAF5), 0.08f to Color(0xffF2EAF5),
0.12f to Color(0xffEDE5EF), 0.12f to Color(0xffEDE5EF),
0.31f to Color(0xffCDB5CD), 0.31f to Color(0xffCDB5CD),
0.49f to Color(0xffBA9BBC), 0.49f to Color(0xffBA9BBC),
1.0f to Color(0xff8D7ACF), 1.0f to Color(0xff8D7ACF),
) ),
innerColors = listOf(
val darkInnerColorGradient = arrayOf(
0.48f to Color(0xffffffff), 0.48f to Color(0xffffffff),
0.63f to Color(0xfff5e4c2), 0.63f to Color(0xfff5e4c2),
0.73f to Color(0xffe5c7ad), 0.73f to Color(0xffe5c7ad),
1.0f to Color(0xffb790a9), 1.0f to Color(0xffb790a9),
) )
),
innerId = R.drawable.geode_monochrome_inner,
outerId = R.drawable.geode_monochrome_outer,
),
darkLogo = R.drawable.geode_base,
lightLogo = R.drawable.geode_base_light,
title = R.string.launcher_title,
titleFont = launcherTitleStyle
)
val sapphireColorPalette = BrandPalette(
darkLogo = R.drawable.sapphire_full,
lightLogo = R.drawable.sapphire_full,
title = R.string.application_icon_sapphire,
titleFont = sapphireTitleStyle
)
@Preview(name = "non-animated, light") @Preview(name = "non-animated, light")
@Preview(name = "non-animated, dark", uiMode = UI_MODE_NIGHT_YES) @Preview(name = "non-animated, dark", uiMode = UI_MODE_NIGHT_YES)
@@ -505,22 +551,49 @@ fun GeodeLogoAnimatedPreview() {
CompositionLocalProvider(LocalTheme provides theme) { CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme) { GeodeLauncherTheme(theme = theme) {
Surface { Surface {
GeodeLogo(true) GeodeLogo(shouldAnimate = true)
}
}
}
}
@Preview(name = "sapphire, non-animated, light")
@Preview(name = "sapphire, non-animated, dark", uiMode = UI_MODE_NIGHT_YES)
@Composable
fun SapphireLogoPreview() {
val theme = if (isSystemInDarkTheme()) DARK else LIGHT
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme) {
Surface {
GeodeLogo(shouldAnimate = false, basePalette = sapphireColorPalette)
}
}
}
}
@Preview(name = "sapphire, animated, light")
@Preview(name = "sapphire, animated, dark", uiMode = UI_MODE_NIGHT_YES)
@Composable
fun SapphireLogoAnimatedPreview() {
val theme = if (isSystemInDarkTheme()) DARK else LIGHT
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme) {
Surface {
GeodeLogo(shouldAnimate = true, basePalette = sapphireColorPalette)
} }
} }
} }
} }
@Composable @Composable
fun AnimatedLogo(modifier: Modifier = Modifier) { fun AnimatedLogo(modifier: Modifier = Modifier, spec: AnimatedIconSpec) {
val theme = LocalTheme.current val theme = LocalTheme.current
val innerColorPalette = remember(theme) {
if (theme == LIGHT) lightInnerColorGradient else darkInnerColorGradient
}
val outerColorPalette = remember(theme) { val colorPalette = if (theme == LIGHT) spec.lightPalette
if (theme == LIGHT) lightOuterColorGradient else darkOuterColorGradient else spec.darkPalette
}
val innerColorPalette = colorPalette.innerColors
val outerColorPalette = colorPalette.outerColors
val infiniteTransition = rememberInfiniteTransition(label = "infinite transition") val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val offset by infiniteTransition.animateFloat( val offset by infiniteTransition.animateFloat(
@@ -534,14 +607,14 @@ fun AnimatedLogo(modifier: Modifier = Modifier) {
) )
val innerBrush = Brush.linearGradient( val innerBrush = Brush.linearGradient(
colorStops = innerColorPalette, colorStops = innerColorPalette.toTypedArray(),
start = Offset(0.0f, 0.0f + (165.06f * offset)), start = Offset(0.0f, 0.0f + (165.06f * offset)),
end = Offset(0.0f, 165.06f + (165.06f * offset)), end = Offset(0.0f, 165.06f + (165.06f * offset)),
tileMode = TileMode.Mirror tileMode = TileMode.Mirror
) )
val outerBrush = Brush.linearGradient( val outerBrush = Brush.linearGradient(
colorStops = outerColorPalette, colorStops = outerColorPalette.toTypedArray(),
start = Offset(0.0f + (84.69f * offset), 0.0f + (165.06f * offset)), start = Offset(0.0f + (84.69f * offset), 0.0f + (165.06f * offset)),
end = Offset(84.69f + (84.69f * offset), 169.61f + (165.06f * offset)), end = Offset(84.69f + (84.69f * offset), 169.61f + (165.06f * offset)),
tileMode = TileMode.Mirror tileMode = TileMode.Mirror
@@ -549,7 +622,7 @@ fun AnimatedLogo(modifier: Modifier = Modifier) {
Box(modifier = modifier) { Box(modifier = modifier) {
Icon( Icon(
painter = painterResource(id = R.drawable.geode_monochrome_inner), painter = painterResource(spec.innerId),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
@@ -560,9 +633,10 @@ fun AnimatedLogo(modifier: Modifier = Modifier) {
blendMode = BlendMode.SrcIn blendMode = BlendMode.SrcIn
) )
} }
.size(64.dp, 64.dp)
) )
Icon( Icon(
painter = painterResource(id = R.drawable.geode_monochrome_outer), painter = painterResource(spec.outerId),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
@@ -573,39 +647,50 @@ fun AnimatedLogo(modifier: Modifier = Modifier) {
blendMode = BlendMode.SrcIn blendMode = BlendMode.SrcIn
) )
} }
.size(64.dp, 64.dp)
) )
} }
} }
@Composable @Composable
fun GeodeLogo(shouldAnimate: Boolean = false, modifier: Modifier = Modifier) { fun GeodeLogo(modifier: Modifier = Modifier, shouldAnimate: Boolean = false, basePalette: BrandPalette = geodeColorPalette) {
Row( FlowRow(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically, itemVerticalAlignment = Alignment.CenterVertically,
modifier = modifier modifier = modifier
) { ) {
val theme = LocalTheme.current val theme = LocalTheme.current
if (basePalette.animatedIcon != null) {
Crossfade( Crossfade(
targetState = shouldAnimate, targetState = shouldAnimate,
animationSpec = tween(durationMillis = 1000, easing = FastOutSlowInEasing), animationSpec = tween(durationMillis = 1000, easing = FastOutSlowInEasing),
label="logo fade" label="logo fade"
) { screen -> ) { screen ->
when (screen) { when (screen) {
true -> AnimatedLogo(modifier = Modifier.size(64.dp, 64.dp)) true -> AnimatedLogo(modifier = Modifier.size(64.dp, 64.dp), spec = basePalette.animatedIcon)
false -> Image( false -> Image(
painterResource(if (theme == LIGHT) painterResource(if (theme == LIGHT)
R.drawable.geode_base_light else R.drawable.geode_base basePalette.lightLogo else basePalette.darkLogo
), ),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(64.dp, 64.dp) modifier = Modifier.size(64.dp, 64.dp)
) )
} }
} }
} else {
Image(
painterResource(if (theme == LIGHT)
basePalette.lightLogo else basePalette.darkLogo
),
contentDescription = null,
modifier = Modifier.size(84.dp, 84.dp)
)
}
Text( Text(
stringResource(R.string.launcher_title), stringResource(basePalette.title),
style = launcherTitleStyle, style = basePalette.titleFont,
fontSize = 64.sp, fontSize = 64.sp,
modifier = Modifier modifier = Modifier
.padding(12.dp) .padding(12.dp)
@@ -693,7 +778,14 @@ fun AltMainScreen(
.padding(8.dp) .padding(8.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
GeodeLogo(shouldAnimate = launchUIState.isInProgress()) val sapphireLogo = remember {
checkIconDate() || PreferenceUtils.get(context)
.getString(PreferenceUtils.Key.SELECTED_ICON) == ApplicationIcon.SAPPHIRE.toId()
}
val basePalette = if (sapphireLogo) sapphireColorPalette
else geodeColorPalette
GeodeLogo(shouldAnimate = launchUIState.isInProgress(), basePalette = basePalette)
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
@@ -13,7 +13,8 @@ import kotlin.time.Instant
data class CrashDump( data class CrashDump(
val filename: String, val filename: String,
val lastModified: Instant val lastModified: Instant,
val fullPath: String,
) )
class CrashViewModel(crashDirectory: File) : BaseDirectoryViewModel<CrashDump>(crashDirectory) { class CrashViewModel(crashDirectory: File) : BaseDirectoryViewModel<CrashDump>(crashDirectory) {
@@ -41,16 +42,19 @@ class CrashViewModel(crashDirectory: File) : BaseDirectoryViewModel<CrashDump>(c
val children = crashDirectory.listFiles { val children = crashDirectory.listFiles {
// ignore indicator files (including old file) // ignore indicator files (including old file)
_, name -> name != LaunchUtils.CRASH_INDICATOR_NAME && name != "last-pid" _, name -> !LaunchUtils.CRASH_INDICATOR_NAMES.contains(name)
} ?: return emptyList() } ?: return emptyList()
hasIndicator = File(crashDirectory, LaunchUtils.CRASH_INDICATOR_NAME).exists() hasIndicator = LaunchUtils.CRASH_INDICATOR_NAMES.any {
File(crashDirectory, it).exists()
}
return children return children
.map { .map {
CrashDump( CrashDump(
filename = it.name, filename = it.name,
lastModified = Instant.fromEpochMilliseconds(it.lastModified()) lastModified = Instant.fromEpochMilliseconds(it.lastModified()),
fullPath = it.path,
) )
} }
.sortedByDescending { it.lastModified } .sortedByDescending { it.lastModified }
@@ -58,7 +62,12 @@ class CrashViewModel(crashDirectory: File) : BaseDirectoryViewModel<CrashDump>(c
fun clearIndicator() { fun clearIndicator() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
File(baseDirectory, LaunchUtils.CRASH_INDICATOR_NAME).delete() LaunchUtils.CRASH_INDICATOR_NAMES.forEach {
val crashFile = File(baseDirectory, it)
if (crashFile.exists()) {
crashFile.delete()
}
}
hasIndicator = false hasIndicator = false
} }
@@ -66,7 +75,8 @@ class CrashViewModel(crashDirectory: File) : BaseDirectoryViewModel<CrashDump>(c
fun createIndicator() { fun createIndicator() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
File(baseDirectory, LaunchUtils.CRASH_INDICATOR_NAME).createNewFile() val crashFile = File(baseDirectory, LaunchUtils.CRASH_INDICATOR_NAMES.first())
crashFile.createNewFile()
hasIndicator = true hasIndicator = true
} }
@@ -14,6 +14,7 @@ data class GeodeLog(
val filename: String, val filename: String,
val lastModified: Instant, val lastModified: Instant,
val fileSize: Long, val fileSize: Long,
val fullPath: String,
) )
class GeodeLogsViewModel(crashDirectory: File) : BaseDirectoryViewModel<GeodeLog>(crashDirectory) { class GeodeLogsViewModel(crashDirectory: File) : BaseDirectoryViewModel<GeodeLog>(crashDirectory) {
@@ -43,7 +44,8 @@ class GeodeLogsViewModel(crashDirectory: File) : BaseDirectoryViewModel<GeodeLog
GeodeLog( GeodeLog(
filename = it.name, filename = it.name,
lastModified = Instant.fromEpochMilliseconds(it.lastModified()), lastModified = Instant.fromEpochMilliseconds(it.lastModified()),
fileSize = it.length() fileSize = it.length(),
fullPath = it.path,
) )
} }
.sortedByDescending { it.lastModified } .sortedByDescending { it.lastModified }
@@ -55,11 +55,14 @@ import com.geode.launcher.preferences.ApplicationLogsActivity
import com.geode.launcher.BuildConfig import com.geode.launcher.BuildConfig
import com.geode.launcher.R import com.geode.launcher.R
import com.geode.launcher.UserDirectoryProvider import com.geode.launcher.UserDirectoryProvider
import com.geode.launcher.preferences.GeodeLogsListingActivity
import com.geode.launcher.preferences.TextViewActivity
import com.geode.launcher.ui.theme.Typography import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.GamePackageUtils import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
data class LoadFailureInfo( data class LoadFailureInfo(
val title: LaunchUtils.LauncherError, val title: LaunchUtils.LauncherError,
@@ -137,14 +140,28 @@ fun ErrorInfoTitle(failureReason: LaunchUtils.LauncherError) {
} }
fun onShareCrash(context: Context) { fun findSomethingForCrash(context: Context): File? {
val lastCrash = LaunchUtils.getLastCrash(context) val lastCrash = LaunchUtils.getLastCrash(context)
if (lastCrash != null) {
return lastCrash
}
val logsDirectory = LaunchUtils.getGeodeLogsDirectory(context)
if (!logsDirectory.exists()) {
return null
}
return logsDirectory.listFiles()?.maxByOrNull { it.lastModified() }
}
fun onShareCrash(context: Context) {
val lastCrash = findSomethingForCrash(context)
if (lastCrash == null) { if (lastCrash == null) {
Toast.makeText(context, R.string.launcher_error_export_missing, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.launcher_error_export_missing, Toast.LENGTH_SHORT).show()
return return
} }
val baseDirectory = LaunchUtils.getBaseDirectory(context) val baseDirectory = LaunchUtils.getBaseDirectory(context, true)
val providerLocation = lastCrash.toRelativeString(baseDirectory) val providerLocation = lastCrash.toRelativeString(baseDirectory)
val documentsPath = "${UserDirectoryProvider.ROOT}${providerLocation}" val documentsPath = "${UserDirectoryProvider.ROOT}${providerLocation}"
@@ -163,6 +180,19 @@ fun onShareCrash(context: Context) {
} }
} }
fun openLastCrash(context: Context) {
val lastCrash = findSomethingForCrash(context)
if (lastCrash == null) {
Toast.makeText(context, R.string.launcher_error_export_missing, Toast.LENGTH_SHORT).show()
return
}
val launchIntent = Intent(context, TextViewActivity::class.java).run {
putExtra(TextViewActivity.EXTRA_LOG_VIEW_FILENAME, lastCrash.path)
}
context.startActivity(launchIntent)
}
fun onShowLogs(context: Context) { fun onShowLogs(context: Context) {
val launchIntent = Intent(context, ApplicationLogsActivity::class.java) val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
context.startActivity(launchIntent) context.startActivity(launchIntent)
@@ -255,6 +285,10 @@ fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
Text(stringResource(R.string.launcher_error_copy)) Text(stringResource(R.string.launcher_error_copy))
} }
} else { } else {
val crashFile = remember { findSomethingForCrash(context) }
val readableCrash = remember { crashFile?.extension == "log" }
if (crashFile != null) {
Button(onClick = { onShareCrash(context) }) { Button(onClick = { onShareCrash(context) }) {
Icon( Icon(
painterResource(R.drawable.icon_share), painterResource(R.drawable.icon_share),
@@ -264,11 +298,21 @@ fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
Text(stringResource(R.string.launcher_error_export_crash)) Text(stringResource(R.string.launcher_error_export_crash))
} }
val developerSettingsEnabled = remember { if (readableCrash) {
PreferenceUtils.get(context).getBoolean(PreferenceUtils.Key.DEVELOPER_MODE) FilledTonalButton(onClick = { openLastCrash(context) }) {
Icon(
painterResource(R.drawable.icon_description),
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_error_view_details))
} }
if (developerSettingsEnabled) { }
FilledTonalButton(onClick = { onShowLogs(context) }) { } else {
Button(onClick = {
val launchIntent = Intent(context, GeodeLogsListingActivity::class.java)
context.startActivity(launchIntent)
}) {
Icon( Icon(
painterResource(R.drawable.icon_description), painterResource(R.drawable.icon_description),
contentDescription = null contentDescription = null
@@ -60,6 +60,7 @@ import com.geode.launcher.utils.ApplicationIcon
import com.geode.launcher.utils.ApplicationIconDetails import com.geode.launcher.utils.ApplicationIconDetails
import com.geode.launcher.utils.IconUtils import com.geode.launcher.utils.IconUtils
import com.geode.launcher.utils.adaptiveIconPainterResource import com.geode.launcher.utils.adaptiveIconPainterResource
import com.geode.launcher.utils.checkIconDate
class ApplicationIconActivity : ComponentActivity() { class ApplicationIconActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -231,15 +232,18 @@ fun ApplicationIconScreen(
) { ) {
val selectedIcon by PreferenceUtils.useStringPreference(PreferenceUtils.Key.SELECTED_ICON) val selectedIcon by PreferenceUtils.useStringPreference(PreferenceUtils.Key.SELECTED_ICON)
val aprilFools = remember { checkIconDate() }
Column(Modifier Column(Modifier
.padding(horizontal = 12.dp) .padding(horizontal = 12.dp)
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
) { ) {
ApplicationIcon.entries.forEach { ApplicationIcon.entries.forEach {
val iconData = IconUtils.getIconDetails(it) val iconData = IconUtils.getIconDetails(it)
if (!iconData.checkDate || aprilFools) {
IconCard(iconData, selectedIcon == iconData.id) IconCard(iconData, selectedIcon == iconData.id)
} }
}
} }
@@ -12,6 +12,7 @@ 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.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -83,7 +84,7 @@ class CrashLogsActivity : ComponentActivity() {
} }
fun shareCrash(context: Context, filename: String) { fun shareCrash(context: Context, filename: String) {
val baseDirectory = LaunchUtils.getBaseDirectory(context) val baseDirectory = LaunchUtils.getBaseDirectory(context, true)
val crashPath = File(LaunchUtils.getCrashDirectory(context), filename) val crashPath = File(LaunchUtils.getCrashDirectory(context), filename)
if (!crashPath.exists()) { if (!crashPath.exists()) {
@@ -110,11 +111,20 @@ fun shareCrash(context: Context, filename: String) {
@Composable @Composable
fun CrashCard(crashDump: CrashDump, crashViewModel: CrashViewModel, modifier: Modifier = Modifier) { fun CrashCard(crashDump: CrashDump, crashViewModel: CrashViewModel, modifier: Modifier = Modifier) {
val isReadable = crashDump.filename.endsWith(".log")
val context = LocalContext.current
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 1.dp, horizontal = 8.dp) .padding(vertical = 1.dp, horizontal = 8.dp)
.height(64.dp) .height(64.dp)
.then(modifier) .then(modifier)
.clickable(isReadable) {
val launchIntent = Intent(context, TextViewActivity::class.java).run {
putExtra(TextViewActivity.EXTRA_LOG_VIEW_FILENAME, crashDump.fullPath)
}
context.startActivity(launchIntent)
}
.background(MaterialTheme.colorScheme.surfaceContainerLow) .background(MaterialTheme.colorScheme.surfaceContainerLow)
.padding(vertical = 8.dp, horizontal = 8.dp) .padding(vertical = 8.dp, horizontal = 8.dp)
) { ) {
@@ -197,20 +197,6 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
onClick = { onOpenLogs(context) } onClick = { onOpenLogs(context) }
) )
OptionsButton(
title = stringResource(R.string.preferences_view_geode_logs),
icon = {
Icon(
painterResource(R.drawable.icon_description),
contentDescription = null
)
},
onClick = {
val launchIntent = Intent(context, GeodeLogsListingActivity::class.java)
context.startActivity(launchIntent)
}
)
OptionsButton( OptionsButton(
title = stringResource(R.string.preferences_copy_external_button), title = stringResource(R.string.preferences_copy_external_button),
description = LaunchUtils.getBaseDirectory(context).path, description = LaunchUtils.getBaseDirectory(context).path,
@@ -80,7 +80,7 @@ class GeodeLogsListingActivity : ComponentActivity() {
} }
fun shareLog(context: Context, filename: String) { fun shareLog(context: Context, filename: String) {
val baseDirectory = LaunchUtils.getBaseDirectory(context) val baseDirectory = LaunchUtils.getBaseDirectory(context, true)
val crashPath = File(LaunchUtils.getGeodeLogsDirectory(context), filename) val crashPath = File(LaunchUtils.getGeodeLogsDirectory(context), filename)
if (!crashPath.exists()) { if (!crashPath.exists()) {
@@ -117,8 +117,8 @@ fun GeodeLogCard(geodeLog: GeodeLog, geodeLogsViewModel: GeodeLogsViewModel, mod
.height(64.dp) .height(64.dp)
.then(modifier) .then(modifier)
.clickable { .clickable {
val launchIntent = Intent(context, GeodeLogViewActivity::class.java).run { val launchIntent = Intent(context, TextViewActivity::class.java).run {
putExtra(GeodeLogViewActivity.EXTRA_LOG_VIEW_FILENAME, geodeLog.filename) putExtra(TextViewActivity.EXTRA_LOG_VIEW_FILENAME, geodeLog.fullPath)
} }
context.startActivity(launchIntent) context.startActivity(launchIntent)
} }
@@ -497,7 +497,7 @@ fun GameplaySettingsGroup() {
SafeModeDialog(onDismiss = { SafeModeDialog(onDismiss = {
showSafeModeDialog = false showSafeModeDialog = false
}) { }) {
val launchIntent = Intent(context, MainActivity::class.java).apply { val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)!!.apply {
// hardcode it, why not // hardcode it, why not
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
data = "geode-launcher://main/launch?safe-mode=true".toUri() data = "geode-launcher://main/launch?safe-mode=true".toUri()
@@ -581,6 +581,20 @@ fun AdvancedSettingsGroup(developerModeEnabled: Boolean, snackbarHostState: Snac
) )
} }
OptionsButton(
title = stringResource(R.string.preferences_view_geode_logs),
icon = {
Icon(
painterResource(R.drawable.icon_description),
contentDescription = null
)
},
onClick = {
val launchIntent = Intent(context, GeodeLogsListingActivity::class.java)
context.startActivity(launchIntent)
}
)
OptionsButton( OptionsButton(
title = stringResource(R.string.preferences_view_crashes), title = stringResource(R.string.preferences_view_crashes),
icon = { icon = {
@@ -1,7 +1,11 @@
package com.geode.launcher.preferences package com.geode.launcher.preferences
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.provider.DocumentsContract
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
@@ -12,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
@@ -35,23 +38,27 @@ 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.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.R import com.geode.launcher.R
import com.geode.launcher.log.GeodeLogsViewModel import com.geode.launcher.UserDirectoryProvider
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.Typography import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.ui.theme.robotoMonoFamily import com.geode.launcher.ui.theme.robotoMonoFamily
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
class GeodeLogViewActivity : ComponentActivity() { class TextViewActivity : ComponentActivity() {
companion object { companion object {
const val EXTRA_LOG_VIEW_FILENAME = "EXTRA_LOG_FILENAME" const val EXTRA_LOG_VIEW_FILENAME = "EXTRA_LOG_FILENAME"
} }
@@ -90,19 +97,48 @@ class GeodeLogViewActivity : ComponentActivity() {
} }
} }
fun onShareFile(context: Context, filename: File) {
val baseDirectory = LaunchUtils.getBaseDirectory(context, true)
val providerLocation = filename.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(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun GeodeLogView( fun GeodeLogView(
onBackPressedDispatcher: OnBackPressedDispatcher?, onBackPressedDispatcher: OnBackPressedDispatcher?,
filename: String, filename: String,
geodeLogsViewModel: GeodeLogsViewModel = viewModel(factory = GeodeLogsViewModel.Factory)
) { ) {
var loadingFile by remember { mutableStateOf(true) } var loadingFile by remember { mutableStateOf(true) }
var description by remember { mutableStateOf(emptyList<String>()) } var description by remember { mutableStateOf(emptyList<String>()) }
val toRead = File(filename)
LaunchedEffect(filename) { LaunchedEffect(filename) {
loadingFile = true loadingFile = true
description = geodeLogsViewModel.getFileText(filename)
withContext(Dispatchers.IO) {
if (!toRead.isFile) {
description = listOf("File does not exist!")
} else {
description = toRead.readLines()
}
}
loadingFile = false loadingFile = false
} }
@@ -123,12 +159,21 @@ fun GeodeLogView(
}, },
title = { title = {
Text( Text(
filename, toRead.name,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
}, },
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
actions = {
val context = LocalContext.current
IconButton(onClick = { onShareFile(context, toRead) }) {
Icon(
painterResource(R.drawable.icon_share),
contentDescription = stringResource(R.string.application_logs_share)
)
}
}
) )
if (loadingFile) { if (loadingFile) {
@@ -16,9 +16,20 @@ val quicksandFamily = FontFamily(
Font(R.font.quicksand_semibold, FontWeight.SemiBold) Font(R.font.quicksand_semibold, FontWeight.SemiBold)
) )
val googleSansFamily = FontFamily(
Font(R.font.googlesans_medium, FontWeight.Medium)
)
val launcherTitleStyle = TextStyle( val launcherTitleStyle = TextStyle(
fontFamily = quicksandFamily, fontFamily = quicksandFamily,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
fontSize = 32.sp, fontSize = 32.sp,
letterSpacing = 0.sp letterSpacing = 0.sp
) )
val sapphireTitleStyle = TextStyle(
fontFamily = googleSansFamily,
fontWeight = FontWeight.Medium,
fontSize = 32.sp,
letterSpacing = 0.sp
)
@@ -62,6 +62,11 @@ abstract class Downloadable {
* Release download link for the current platform * Release download link for the current platform
*/ */
abstract fun getDownload(): DownloadableAsset? abstract fun getDownload(): DownloadableAsset?
/**
* Resources download link for current platform.
*/
abstract fun getResourcesDownload(): DownloadableAsset?
} }
class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadable() { class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadable() {
@@ -101,6 +106,18 @@ class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadab
size = download.size.toLong() size = download.size.toLong()
) )
} }
override fun getResourcesDownload(): DownloadableAsset? {
val download = release.assets.find {
it.name == "resources.zip"
} ?: return null
return DownloadableAsset(
url = download.browserDownloadUrl,
filename = download.name,
size = download.size.toLong()
)
}
} }
class DownloadableLauncherRelease(val release: Release) : Downloadable() { class DownloadableLauncherRelease(val release: Release) : Downloadable() {
@@ -126,8 +143,7 @@ class DownloadableLauncherRelease(val release: Release) : Downloadable() {
* 0 | 0 | 1 * 0 | 0 | 1
* surprise! it's an xnor * surprise! it's an xnor
*/ */
asset.name.endsWith("apk") && asset.name.contains("android32") == use32BitPlatform
(asset.name.contains("android32")) == use32BitPlatform
} }
} }
@@ -139,6 +155,10 @@ class DownloadableLauncherRelease(val release: Release) : Downloadable() {
size = download.size.toLong() size = download.size.toLong()
) )
} }
override fun getResourcesDownload(): DownloadableAsset? {
return null
}
} }
class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadable() { class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadable() {
@@ -151,11 +171,19 @@ class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadab
return version.createdAt.epochSeconds return version.createdAt.epochSeconds
} }
override fun getDownload(): DownloadableAsset? { override fun getDownload(): DownloadableAsset {
val filename = "geode-${version.tag}-${LaunchUtils.platformName}.zip" val filename = "geode-${version.tag}-${LaunchUtils.platformName}.zip"
return DownloadableAsset( return DownloadableAsset(
url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/$filename", url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/$filename",
filename = filename filename = filename
) )
} }
override fun getResourcesDownload(): DownloadableAsset {
val filename = "resources.zip"
return DownloadableAsset(
url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/resources.zip",
filename = filename
)
}
} }
@@ -18,12 +18,16 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.Cache import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okio.HashingSink
import okio.blackholeSink
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import okio.source import okio.source
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException
import java.io.InterruptedIOException import java.io.InterruptedIOException
import kotlin.math.max
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
@@ -94,7 +98,10 @@ class ReleaseManager private constructor(
var dismissedLauncherUpdate = false var dismissedLauncherUpdate = false
var skipStateUpdate = false
private fun sendError(e: Exception) { private fun sendError(e: Exception) {
skipStateUpdate = true
_uiState.value = ReleaseManagerState.Failure(e) _uiState.value = ReleaseManagerState.Failure(e)
// ignore cancellation, it's good actually // ignore cancellation, it's good actually
@@ -166,7 +173,59 @@ class ReleaseManager private constructor(
return Result.success(outputFile) return Result.success(outputFile)
} }
private suspend fun performResourceDownload(resourceAsset: DownloadableAsset, initialSize: Long): Boolean {
val guessSize = if (resourceAsset.size == null) null else initialSize + resourceAsset.size
val guessInitial = if (guessSize == null) 0 else initialSize
_uiState.value = ReleaseManagerState.InDownload(guessInitial, guessSize)
val outputDir = getTempResourcesDirectory()
val outputPath = outputDir.path
val finalDir = LaunchUtils.getGeodeResourcesDirectory(applicationContext)
try {
if (!outputDir.exists()) {
outputDir.mkdirs()
}
DownloadUtils.downloadStream(
httpClient,
resourceAsset.url,
onProgress = { progress, outOf ->
if (!skipStateUpdate)
_uiState.value = ReleaseManagerState.InDownload(initialSize + progress, outOf + initialSize)
},
onResponse = { body ->
DownloadUtils.copyZipStreamToDirectory(
body.byteStream(),
outputDir
)
if (finalDir.exists()) {
finalDir.deleteRecursively()
}
if (!outputDir.renameTo(finalDir)) {
throw IOException("failed to rename resources output directory")
}
}
)
} catch (e: Exception) {
sendError(e)
return false
} finally {
val tempPathClone = File(outputPath)
runCatching {
if (tempPathClone.exists()) tempPathClone.deleteRecursively()
}
}
return true
}
private suspend fun performUpdate(release: Downloadable) { private suspend fun performUpdate(release: Downloadable) {
skipStateUpdate = false
val releaseAsset = release.getDownload() val releaseAsset = release.getDownload()
if (releaseAsset == null) { if (releaseAsset == null) {
val noAssetException = Exception("missing Android download") val noAssetException = Exception("missing Android download")
@@ -175,8 +234,14 @@ class ReleaseManager private constructor(
return return
} }
val resourcesAsset = release.getResourcesDownload()
var releaseSize = releaseAsset.size ?: 0L
val resourcesSize = resourcesAsset?.size ?: 0L
// set an initial download size // set an initial download size
_uiState.value = ReleaseManagerState.InDownload(0, releaseAsset.size) val initialSize = if (releaseAsset.size == null && resourcesAsset?.size == null) null else releaseSize + resourcesSize
_uiState.value = ReleaseManagerState.InDownload(0, initialSize)
val outputFile = getTempFile() val outputFile = getTempFile()
// clone the file instance as renameTo may move the original file // clone the file instance as renameTo may move the original file
@@ -195,7 +260,10 @@ class ReleaseManager private constructor(
httpClient, httpClient,
releaseAsset.url, releaseAsset.url,
onProgress = { progress, outOf -> onProgress = { progress, outOf ->
_uiState.value = ReleaseManagerState.InDownload(progress, outOf) if (!skipStateUpdate)
_uiState.value = ReleaseManagerState.InDownload(progress, outOf + resourcesSize)
releaseSize = max(outOf, releaseSize)
}, },
onResponse = { body -> onResponse = { body ->
DownloadUtils.extractFileFromZipStream( DownloadUtils.extractFileFromZipStream(
@@ -229,6 +297,12 @@ class ReleaseManager private constructor(
} }
} }
if (resourcesAsset != null) {
if (!performResourceDownload(resourcesAsset, releaseSize)) {
return
}
}
// extraction performed // extraction performed
updatePreferences(release) updatePreferences(release)
@@ -249,10 +323,9 @@ class ReleaseManager private constructor(
val sharedPreferences = PreferenceUtils.get(applicationContext) val sharedPreferences = PreferenceUtils.get(applicationContext)
val originalFileHash = sharedPreferences.getString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED) val originalFileHash = sharedPreferences.getString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED)
?: return false ?: return true
val currentFileHash = computeFileHash(geodeFile) val currentFileHash = computeFileHash(geodeFile)
return originalFileHash != currentFileHash return originalFileHash != currentFileHash
} }
@@ -384,8 +457,14 @@ class ReleaseManager private constructor(
sharedPreferences.setString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED, fileHash) sharedPreferences.setString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED, fileHash)
} }
private fun computeFileHash(file: File): String = private fun computeFileHash(file: File): String {
file.source().buffer().use { it.readByteString().md5().hex() } val sink = HashingSink.md5(blackholeSink())
file.source().buffer().use {
it.readAll(sink)
}
return sink.hash.hex()
}
private fun getGeodeOutputPath(): File { private fun getGeodeOutputPath(): File {
val geodeName = LaunchUtils.geodeFilename val geodeName = LaunchUtils.geodeFilename
@@ -394,6 +473,13 @@ class ReleaseManager private constructor(
return File(geodeDirectory, geodeName) return File(geodeDirectory, geodeName)
} }
private fun createRandomString(): String {
val alphabet = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return buildString(8) {
repeat(8) { append(alphabet.random()) }
}
}
private fun getTempFile(): File { private fun getTempFile(): File {
val geodeName = LaunchUtils.geodeFilename val geodeName = LaunchUtils.geodeFilename
val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext) val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext)
@@ -401,11 +487,7 @@ class ReleaseManager private constructor(
// warning!! while File::createTempFile may look tempting, a certain brand of phones has a messed up implementation of it // warning!! while File::createTempFile may look tempting, a certain brand of phones has a messed up implementation of it
// so we're making a temp file manually (as long as it doesn't collide with the geode download, it's okay) // so we're making a temp file manually (as long as it doesn't collide with the geode download, it's okay)
val alphabet = ('A'..'Z') + ('a'..'z') + ('0'..'9') val suffix = createRandomString()
val suffix = buildString(8) {
repeat(8) { append(alphabet.random()) }
}
val tmpName = "tmp-$suffix.$geodeName" val tmpName = "tmp-$suffix.$geodeName"
val tempFile = File(geodeDirectory, tmpName) val tempFile = File(geodeDirectory, tmpName)
@@ -413,6 +495,19 @@ class ReleaseManager private constructor(
return tempFile return tempFile
} }
private fun getTempResourcesDirectory(): File {
val finalDir = LaunchUtils.getGeodeResourcesDirectory(applicationContext)
val suffix = createRandomString()
val tempDirName = "tmp-$suffix-geode.loader"
val parentDir = finalDir.parentFile
return if (parentDir != null) {
File(finalDir.parentFile, tempDirName)
} else {
File(tempDirName)
}
}
/** /**
* Cancels the current update job. * Cancels the current update job.
*/ */
@@ -17,7 +17,7 @@ object Constants {
const val SUPPORTED_VERSION_CODE_MIN = 37L const val SUPPORTED_VERSION_CODE_MIN = 37L
// anything below this version code shows a warning on the main menu // anything below this version code shows a warning on the main menu
const val SUPPORTED_VERSION_CODE_MIN_WARNING = 40L const val SUPPORTED_VERSION_CODE_MIN_WARNING = 41L
// anything above this version code will show a warning when starting the game // anything above this version code will show a warning when starting the game
const val SUPPORTED_VERSION_CODE = 41L const val SUPPORTED_VERSION_CODE = 41L
@@ -72,6 +72,35 @@ object DownloadUtils {
} }
} }
suspend fun copyZipStreamToDirectory(inputStream: InputStream, output: File) = runInterruptible {
ZipInputStream(inputStream).use { zipStream ->
if (output.exists()) output.deleteRecursively()
output.mkdirs()
val canonicalOutput = output.canonicalPath
var entry = zipStream.nextEntry
while (entry != null) {
val destination = File(output, entry.name)
if (!destination.canonicalPath.startsWith(canonicalOutput)) {
throw IOException("attempted copy to outside of output directory: ${entry.name}")
}
if (destination.isDirectory) {
destination.mkdirs()
} else {
destination.parentFile?.mkdirs()
destination.outputStream().use { destinationStream ->
zipStream.copyTo(destinationStream)
}
}
entry = zipStream.nextEntry
}
}
}
/** /**
* Extracts a file named by zipPath from inputStream and copies it to outputStream * Extracts a file named by zipPath from inputStream and copies it to outputStream
* *
@@ -15,6 +15,10 @@ import androidx.compose.ui.res.painterResource
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import com.geode.launcher.R import com.geode.launcher.R
import kotlinx.datetime.Month
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
import kotlin.time.Clock
// https://gist.github.com/tkuenneth/ddf598663f041dc79960cda503d14448?permalink_comment_id=4660486#gistcomment-4660486 // https://gist.github.com/tkuenneth/ddf598663f041dc79960cda503d14448?permalink_comment_id=4660486#gistcomment-4660486
@Composable @Composable
@@ -43,31 +47,40 @@ enum class ApplicationIcon {
DEFAULT, DEFAULT,
GEODE, GEODE,
PRIDE, PRIDE,
TRANS; TRANS,
SAPPHIRE;
companion object { companion object {
fun fromId(id: String) = when (id) { fun fromId(id: String) = when (id) {
"default" -> ApplicationIcon.DEFAULT "default" -> DEFAULT
"geode" -> ApplicationIcon.GEODE "geode" -> GEODE
"pride" -> ApplicationIcon.PRIDE "pride" -> PRIDE
"trans" -> ApplicationIcon.TRANS "trans" -> TRANS
else -> ApplicationIcon.DEFAULT "sapphire" -> SAPPHIRE
else -> DEFAULT
} }
} }
fun toId() = when (this) { fun toId() = when (this) {
ApplicationIcon.PRIDE -> "pride" PRIDE -> "pride"
ApplicationIcon.GEODE -> "geode" GEODE -> "geode"
ApplicationIcon.DEFAULT -> "default" DEFAULT -> "default"
ApplicationIcon.TRANS -> "trans" TRANS -> "trans"
SAPPHIRE -> "sapphire"
} }
} }
fun checkIconDate(): Boolean {
val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault())
return currentDate.month == Month.APRIL && currentDate.day <= 7
}
data class ApplicationIconDetails( data class ApplicationIconDetails(
val id: String, val id: String,
val component: String, val component: String,
@param:DrawableRes val iconId: Int, @param:DrawableRes val iconId: Int,
@param:StringRes val nameId: Int, @param:StringRes val nameId: Int,
val checkDate: Boolean = false,
) )
object IconUtils { object IconUtils {
@@ -96,5 +109,12 @@ object IconUtils {
iconId = R.mipmap.ic_launcher_trans, iconId = R.mipmap.ic_launcher_trans,
nameId = R.string.application_icon_trans nameId = R.string.application_icon_trans
) )
ApplicationIcon.SAPPHIRE -> ApplicationIconDetails(
id = id.toId(),
component = "MainActivitySapphire",
iconId = R.mipmap.ic_launcher_sapphire,
nameId = R.string.application_icon_sapphire,
checkDate = true
)
} }
} }
@@ -73,7 +73,7 @@ object LaunchUtils {
return File(getBaseDirectory(context), "save") return File(getBaseDirectory(context), "save")
} }
const val CRASH_INDICATOR_NAME = "lastSessionDidCrash" val CRASH_INDICATOR_NAMES = setOf("lastSessionDidCrash", "last-pid", "last-crashed")
fun getCrashDirectory(context: Context): File { fun getCrashDirectory(context: Context): File {
val base = getBaseDirectory(context) val base = getBaseDirectory(context)
@@ -85,6 +85,11 @@ object LaunchUtils {
return File(base, "game/geode/logs/") return File(base, "game/geode/logs/")
} }
fun getGeodeResourcesDirectory(context: Context): File {
val base = getBaseDirectory(context)
return File(base, "game/geode/resources/geode.loader/")
}
fun getLastCrash(context: Context): File? { fun getLastCrash(context: Context): File? {
val crashDirectory = getCrashDirectory(context) val crashDirectory = getCrashDirectory(context)
if (!crashDirectory.exists()) { if (!crashDirectory.exists()) {
@@ -93,7 +98,7 @@ object LaunchUtils {
val children = crashDirectory.listFiles { val children = crashDirectory.listFiles {
// ignore indicator files (including old file) // ignore indicator files (including old file)
_, name -> name != CRASH_INDICATOR_NAME && name != "last-pid" _, name -> !CRASH_INDICATOR_NAMES.contains(name)
} }
return children?.maxByOrNull { it.lastModified() } return children?.maxByOrNull { it.lastModified() }
@@ -101,9 +106,9 @@ object LaunchUtils {
fun lastSessionCrashed(context: Context): Boolean { fun lastSessionCrashed(context: Context): Boolean {
val base = getCrashDirectory(context) val base = getCrashDirectory(context)
val crashIndicatorFile = File(base, CRASH_INDICATOR_NAME) return CRASH_INDICATOR_NAMES.any {
File(base, it).exists()
return crashIndicatorFile.exists() }
} }
enum class LauncherError : Serializable { enum class LauncherError : Serializable {
@@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M0,0h1024v1024h-1024z"
android:fillColor="#070066"/>
<path
android:pathData="M512.5,521.1L915.2,638.7L1082,922.6L915.2,1206.4L512.5,1324L109.8,1206.4L-57,922.6L109.8,638.7L512.5,521.1Z"
android:fillColor="#6694C8"/>
<path
android:pathData="M915.2,638.7L1082,922.6V638.7H915.2Z"
android:fillColor="#4268B8"/>
<path
android:pathData="M109.8,638.7L512.5,521.1L109.8,397.2V638.7Z"
android:fillColor="#2D5EC7"/>
<path
android:pathData="M-57,922.6L109.8,638.7H-57V922.6Z"
android:fillColor="#4268B8"/>
<path
android:pathData="M512.5,521.1L915.2,638.7V397.2L512.5,521.1Z"
android:fillColor="#2D5EC7"/>
<path
android:pathData="M-57,638.7H109.8V397.2L-57,638.7Z"
android:fillColor="#2248AA"/>
<path
android:pathData="M109.8,397.2L512.5,521.1V194L109.8,397.2Z"
android:fillColor="#020F9D"/>
<path
android:pathData="M512.5,521.1L915.2,397.2L512.5,194V521.1Z"
android:fillColor="#020F9D"/>
<path
android:pathData="M915.2,638.7H1082L915.2,397.2V638.7Z"
android:fillColor="#2248AA"/>
</vector>
@@ -0,0 +1,27 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="643"
android:viewportHeight="766">
<group android:scaleX="0.41038585"
android:scaleY="0.4888889"
android:translateX="191.0163"
android:translateY="195.75555">
<path
android:pathData="M299.94,33.78C313.01,25.85 329.41,25.85 342.48,33.78L370.9,51.03L268.17,116.49L268.23,116.51L92.68,223.61C84.64,228.51 79.74,237.25 79.74,246.66V509.84C79.74,519.26 84.64,527.99 92.68,532.89L306.22,663.17C314.85,668.44 325.71,668.44 334.34,663.17L547.89,532.89C555.92,527.99 560.82,519.26 560.82,509.85V246.66C560.82,246.2 560.81,245.74 560.78,245.28V166.26L599.75,189.9C612,197.34 619.48,210.63 619.48,224.96V540.81C619.48,555.14 612,568.42 599.75,575.86L342.48,731.98C329.41,739.91 313.01,739.91 299.94,731.98L42.66,575.86C30.41,568.42 22.93,555.14 22.93,540.81V224.96C22.93,210.63 30.41,197.34 42.66,189.9L299.94,33.78Z"
android:fillColor="#000000"
android:fillAlpha="0.15"/>
<path
android:pathData="M311.75,173.17C318.78,168.88 327.62,168.88 334.65,173.17L481.88,262.87C488.43,266.86 492.43,273.98 492.43,281.66V463.16C492.43,470.84 488.43,477.96 481.88,481.95L334.65,571.65C327.62,575.94 318.78,575.94 311.75,571.65L164.52,481.95C157.97,477.96 153.97,470.84 153.97,463.16V414.95L208.24,448.54C208.52,448.75 208.8,448.95 209.1,449.13L315.66,515.36C319.22,517.57 323.72,517.57 327.28,515.36L433.84,449.13C437.07,447.13 439.03,443.59 439.03,439.79V305.9C439.03,302.09 437.07,298.56 433.84,296.55L327.28,230.33C323.72,228.12 319.22,228.12 315.66,230.33L209.1,296.55C208.62,296.85 208.17,297.18 207.75,297.55L153.97,331.28V281.66C153.97,273.98 157.97,266.86 164.52,262.87L311.75,173.17Z"
android:fillColor="#000000"
android:fillAlpha="0.15"
android:fillType="evenOdd"/>
<path
android:pathData="M299.94,5.95C313.01,-1.98 329.41,-1.98 342.48,5.95L374.73,25.52L264.09,96.01L264.15,96.03L74.11,211.97C66.07,216.88 61.17,225.61 61.17,235.02V520.77C61.17,530.18 66.07,538.92 74.11,543.82L306.15,685.38C314.78,690.65 325.64,690.65 334.27,685.38L566.31,543.82C574.34,538.92 579.25,530.18 579.25,520.77V235.02C579.25,234.53 579.23,234.05 579.2,233.56V149.6L622.68,175.99C634.93,183.42 642.41,196.71 642.41,211.04V554.72C642.41,569.05 634.93,582.34 622.68,589.78L342.48,759.82C329.41,767.75 313.01,767.75 299.94,759.82L19.73,589.78C7.48,582.34 0,569.05 0,554.72V211.04C0,196.71 7.48,183.42 19.73,175.99L299.94,5.95Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M311.75,142.59C318.78,138.31 327.62,138.31 334.65,142.59L507.79,248.08C514.34,252.08 518.34,259.2 518.34,266.87V479.95C518.34,487.62 514.34,494.74 507.79,498.74L334.65,604.23C327.62,608.51 318.78,608.51 311.75,604.23L138.61,498.74C132.06,494.74 128.06,487.62 128.06,479.95V422.46L189.93,460.76C190.22,460.98 190.52,461.19 190.84,461.39L315.4,538.8C318.95,541.01 323.46,541.01 327.01,538.8L451.58,461.39C454.8,459.38 456.77,455.85 456.77,452.04V295.77C456.77,291.97 454.8,288.44 451.58,286.43L327.01,209.02C323.46,206.81 318.95,206.81 315.4,209.02L190.84,286.43C190.32,286.76 189.83,287.12 189.37,287.52L128.06,325.98V266.87C128.06,259.2 132.06,252.08 138.61,248.08L311.75,142.59Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</group>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

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_sapphire_background"/>
<foreground android:drawable="@drawable/ic_launcher_sapphire_foreground"/>
</adaptive-icon>
+4 -2
View File
@@ -53,6 +53,7 @@
<string name="launcher_error_view_logs">View logs</string> <string name="launcher_error_view_logs">View logs</string>
<string name="launcher_error_copy_error_alt">Copy</string> <string name="launcher_error_copy_error_alt">Copy</string>
<string name="launcher_error_copy">Copy details</string> <string name="launcher_error_copy">Copy details</string>
<string name="launcher_error_view_details">View details</string>
<string name="launcher_error_export_crash">Share crash</string> <string name="launcher_error_export_crash">Share crash</string>
<string name="launcher_error_export_missing">No crash dump was found!</string> <string name="launcher_error_export_missing">No crash dump was found!</string>
<string name="launcher_error_device_info">Android %2$s, %1$s (%5$s)\nGD %4$s, Geode %3$s + v%6$s</string> <string name="launcher_error_device_info">Android %2$s, %1$s (%5$s)\nGD %4$s, Geode %3$s + v%6$s</string>
@@ -118,10 +119,10 @@
<string name="load_failed_crashed_description">A crash was reported during the last session. Try to:</string> <string name="load_failed_crashed_description">A crash was reported during the last session. Try to:</string>
<string name="load_failed_recommendation_reinstall">Reinstall Geometry Dash.</string> <string name="load_failed_recommendation_reinstall">Reinstall Geometry Dash.</string>
<string name="load_failed_recommendation_switch_32bit_abi">Install the 32-bit version of the launcher.</string> <string name="load_failed_recommendation_switch_32bit_abi">Install the 32-bit version of the launcher.</string>
<string name="load_failed_recommendation_switch_64bit_abi">Install the non 32-bit version of the launcher.</string> <string name="load_failed_recommendation_switch_64bit_abi">Install the non-32-bit version of the launcher.</string>
<string name="load_failed_recommendation_update">Verify that the latest versions of the launcher and Geometry Dash are installed.</string> <string name="load_failed_recommendation_update">Verify that the latest versions of the launcher and Geometry Dash are installed.</string>
<string name="load_failed_recommendation_report">Report this issue to the developers.</string> <string name="load_failed_recommendation_report">Report this issue to the developers.</string>
<string name="load_failed_recommendation_safe_mode">Enable the safe mode button in Settings.</string> <string name="load_failed_recommendation_safe_mode">Use the safe mode button in Settings.</string>
<string name="load_failed_recommendation_dev_stl">Devs: If this is a locally built Geode version, please use the c++_shared ANDROID_STL!</string> <string name="load_failed_recommendation_dev_stl">Devs: If this is a locally built Geode version, please use the c++_shared ANDROID_STL!</string>
<string name="message_box_cancel">Cancel</string> <string name="message_box_cancel">Cancel</string>
@@ -335,6 +336,7 @@
<string name="application_icon_geode">Geode</string> <string name="application_icon_geode">Geode</string>
<string name="application_icon_pride">Pride</string> <string name="application_icon_pride">Pride</string>
<string name="application_icon_trans">Transgender</string> <string name="application_icon_trans">Transgender</string>
<string name="application_icon_sapphire">Sapphire</string>
<string name="application_icon_confirm">Change icon to %1$s?</string> <string name="application_icon_confirm">Change icon to %1$s?</string>
<string name="application_icon_confirm_description">Geode Launcher will close itself to change the application icon.</string> <string name="application_icon_confirm_description">Geode Launcher will close itself to change the application icon.</string>
+3 -3
View File
@@ -1,13 +1,13 @@
buildscript { buildscript {
val composeBOM by extra("2026.02.00") val composeBOM by extra("2026.03.01")
} }
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id("com.android.application") version "9.0.1" apply false id("com.android.application") version "9.0.1" apply false
id("com.android.library") version "9.0.1" apply false id("com.android.library") version "9.0.1" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "2.3.10" apply false id("org.jetbrains.kotlin.plugin.serialization") version "2.3.20" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.3.20" apply false
} }
tasks.register<Delete>("clean") { tasks.register<Delete>("clean") {