├── proguard
├── oshi.pro
├── log4j.pro
├── slf4j.pro
├── kotlinlogging.pro
├── jna.pro
├── compose.pro
├── HackedSelectionContainer.kt.pro
└── kotlinx-serialization.pro
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── .idea
└── copyright
│ ├── profiles_settings.xml
│ └── Server_List_Explorer_GPT_3_0.xml
├── ui
├── src
│ └── main
│ │ ├── composeResources
│ │ └── drawable
│ │ │ ├── texture_unknown_server.png
│ │ │ ├── flag_uk.svg
│ │ │ └── flag_us.svg
│ │ └── kotlin
│ │ └── com
│ │ └── spoiligaming
│ │ └── explorer
│ │ └── ui
│ │ ├── extensions
│ │ ├── IntExtensions.kt
│ │ ├── LongExtensions.kt
│ │ ├── InputStreamExtensions.kt
│ │ ├── ColorExtensions.kt
│ │ ├── StringExtensions.kt
│ │ ├── ImageBitmapExtensions.kt
│ │ └── ImageBitmapLuminance.kt
│ │ ├── navigation
│ │ ├── Destinations.kt
│ │ └── NavGraph.kt
│ │ ├── screens
│ │ ├── multiplayer
│ │ │ ├── ServerEntryMoveDirection.kt
│ │ │ ├── querymethod
│ │ │ │ ├── QueryMethodModels.kt
│ │ │ │ ├── items
│ │ │ │ │ ├── QueryMethodCheckboxItem.kt
│ │ │ │ │ └── QueryMethodTimeoutItem.kt
│ │ │ │ ├── QueryMethodConfigurationTooltip.kt
│ │ │ │ └── QueryMethodChipPresets.kt
│ │ │ ├── ShimmerServerEntry.kt
│ │ │ └── ServerSelectionController.kt
│ │ ├── setup
│ │ │ ├── steps
│ │ │ │ └── LanguageSelectionStep.kt
│ │ │ └── SetupStepContainer.kt
│ │ └── settings
│ │ │ └── components
│ │ │ └── SettingsSection.kt
│ │ ├── theme
│ │ └── Color.kt
│ │ ├── widgets
│ │ ├── ItemSelectableDropdownMenu.kt
│ │ ├── SlickTextButton.kt
│ │ ├── ItemSwitch.kt
│ │ └── PocketInfoTooltip.kt
│ │ ├── util
│ │ ├── FilePickerUtil.kt
│ │ └── AdaptiveDimension.kt
│ │ ├── snackbar
│ │ ├── SnackbarHostManager.kt
│ │ └── SnackbarController.kt
│ │ ├── Localization.kt
│ │ ├── components
│ │ ├── FPSCounterOverlay.kt
│ │ └── MarkdownAutoLinkText.kt
│ │ └── window
│ │ ├── WindowState.kt
│ │ └── WindowEffects.kt
└── build.gradle.kts
├── gradle.properties
├── .editorconfig
├── .run
├── SLE (Production).run.xml
├── SLE (Debug).run.xml
└── Generate Compose Resources.run.xml
├── .gitignore
├── nbt
├── build.gradle.kts
└── src
│ └── main
│ └── kotlin
│ └── com
│ └── spoiligaming
│ └── explorer
│ └── multiplayer
│ ├── MultiplayerServer.kt
│ └── history
│ └── ServerListHistoryService.kt
├── core
├── src
│ ├── main
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── spoiligaming
│ │ │ └── explorer
│ │ │ ├── minecraft
│ │ │ ├── multiplayer
│ │ │ │ ├── online
│ │ │ │ │ └── backend
│ │ │ │ │ │ ├── common
│ │ │ │ │ │ ├── IServerData.kt
│ │ │ │ │ │ ├── OfflineServerData.kt
│ │ │ │ │ │ ├── McSrvStatRateLimitedServerData.kt
│ │ │ │ │ │ ├── IServerQueryHandler.kt
│ │ │ │ │ │ ├── OnlineServerDataResourceResult.kt
│ │ │ │ │ │ └── OnlineServerDataModels.kt
│ │ │ │ │ │ ├── mcsrvstat
│ │ │ │ │ │ ├── OnlineServerDataDeserializer.kt
│ │ │ │ │ │ ├── McSrvStatServerQueryHandler.kt
│ │ │ │ │ │ ├── RequestHandler.kt
│ │ │ │ │ │ └── McSrvStatServerStatusResponse.kt
│ │ │ │ │ │ └── mcutils
│ │ │ │ │ │ └── retry.kt
│ │ │ │ ├── ServerListModule.kt
│ │ │ │ └── ServerListPaths.kt
│ │ │ ├── common
│ │ │ │ ├── IModuleKind.kt
│ │ │ │ ├── IModeLoader.kt
│ │ │ │ ├── PathResolver.kt
│ │ │ │ └── UnifiedModeLoader.kt
│ │ │ └── singleplayer
│ │ │ │ ├── SingleplayerModule.kt
│ │ │ │ └── WorldDirectoryPaths.kt
│ │ │ └── util
│ │ │ ├── StringExtensions.kt
│ │ │ └── DurationFormatting.kt
│ └── test
│ │ └── kotlin
│ │ └── com
│ │ └── spoiligaming
│ │ └── explorer
│ │ └── minecraft
│ │ └── multiplayer
│ │ └── online
│ │ └── backend
│ │ └── mcsrvstat
│ │ └── McSrvStatServerStatusResponseTest.kt
└── build.gradle.kts
├── settings
├── build.gradle.kts
└── src
│ └── main
│ └── kotlin
│ └── com
│ └── spoiligaming
│ └── explorer
│ └── settings
│ ├── manager
│ ├── SettingsManager.kt
│ └── SettingsManagers.kt
│ ├── model
│ ├── SingleplayerSettings.kt
│ ├── WindowState.kt
│ ├── ThemeSettings.kt
│ ├── MultiplayerSettings.kt
│ ├── Preferences.kt
│ ├── WindowAppearence.kt
│ └── ServerQueryMethodConfigurations.kt
│ ├── serializer
│ ├── LocaleSerializer.kt
│ └── PathSerializer.kt
│ └── util
│ └── SettingsFile.kt
├── app
└── src
│ ├── test
│ └── resources
│ │ └── log4j2-test.xml
│ └── main
│ ├── kotlin
│ └── com
│ │ └── spoiligaming
│ │ └── explorer
│ │ ├── Main.kt
│ │ ├── ArgsParser.kt
│ │ └── LogStorage.kt
│ └── resources
│ ├── log4j2-prod.xml
│ └── log4j2-dev.xml
├── settings.gradle.kts
├── .github
└── workflows
│ └── verify-pr.yml
├── gradlew.bat
└── add_gpl_header.bat
/proguard/oshi.pro:
--------------------------------------------------------------------------------
1 | -keep class oshi.driver.windows.wmi.** { *; }
2 |
--------------------------------------------------------------------------------
/proguard/log4j.pro:
--------------------------------------------------------------------------------
1 | -keep class org.apache.logging.log4j.** { *; }
2 |
--------------------------------------------------------------------------------
/proguard/slf4j.pro:
--------------------------------------------------------------------------------
1 | -keep class org.slf4j.** { *; }
2 | -keep class org.apache.logging.slf4j.SLF4JServiceProvider { *; }
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SpoilerRules/server-list-explorer/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/ui/src/main/composeResources/drawable/texture_unknown_server.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SpoilerRules/server-list-explorer/HEAD/ui/src/main/composeResources/drawable/texture_unknown_server.png
--------------------------------------------------------------------------------
/proguard/kotlinlogging.pro:
--------------------------------------------------------------------------------
1 | -assumenosideeffects class io.github.oshai.kotlinlogging.logback.** { *; }
2 | -dontnote io.github.oshai.kotlinlogging.logback.**
3 | -dontwarn io.github.oshai.kotlinlogging.logback.**
4 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2 | org.gradle.parallel=true
3 | org.gradle.configuration-cache=true
4 | org.gradle.caching=true
5 | org.gradle.java.installations.auto-download=true
6 | kotlin.code.style=official
7 |
8 |
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/proguard/jna.pro:
--------------------------------------------------------------------------------
1 | -keep class com.sun.jna.** {
2 | *;
3 | }
4 | -keepclassmembers class com.sun.jna.** {
5 | native ;
6 | }
7 |
8 | -keep class com.sun.jna.platform.** {
9 | *;
10 | }
11 |
12 | -keepclassmembers class * extends com.sun.jna.Structure {
13 | ;
14 | ;
15 | }
16 |
17 | -keep class * extends com.sun.jna.Library { *; }
18 | -keep class * extends com.sun.jna.Callback { *; }
19 |
--------------------------------------------------------------------------------
/proguard/compose.pro:
--------------------------------------------------------------------------------
1 | -keep class androidx.compose.ui.input.pointer.** { *; }
2 | -keep class androidx.compose.ui.window.** { *; }
3 |
4 | # Required: Used via reflection in FloatingDialogBuilder.kt
5 | # - Class.forName("androidx.compose.material3.TooltipScopeImpl") and its constructor
6 | # are invoked explicitly, so the class name and members must remain unchanged.
7 | -keep class androidx.compose.material3.TooltipScopeImpl {
8 | (...);
9 | *;
10 | }
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | charset = utf-8
6 | trim_trailing_whitespace = true
7 |
8 | [*.{kt,kts}]
9 | indent_style = space
10 | indent_size = 4
11 | insert_final_newline = true
12 | no-wildcard-imports = true
13 | ktlint_function_naming_ignore_when_annotated_with = Composable
14 | ktlint_standard_property-naming = disabled
15 | ktlint_standard_filename = disabled
16 | max_line_length = 120
17 |
18 | [**/generated/**]
19 | generated_code = true
20 | ij_formatter_enabled = false
21 | ktlint = disabled
22 |
--------------------------------------------------------------------------------
/.run/SLE (Production).run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .kotlin/
2 | workspace/
3 |
4 | .gradle
5 | /.gradle-user-home/**/*
6 | build/
7 | !gradle/wrapper/gradle-wrapper.jar
8 | !**/src/main/**/build/
9 | !**/src/test/**/build/
10 |
11 | ### IntelliJ IDEA ###
12 | .idea/*
13 | !.idea/
14 | !.idea/copyright/
15 | !.idea/copyright/*.xml
16 | *.iws
17 | *.iml
18 | *.ipr
19 | out/
20 | !**/src/main/**/out/
21 | !**/src/test/**/out/
22 |
23 | ### Eclipse ###
24 | .apt_generated
25 | .classpath
26 | .factorypath
27 | .project
28 | .settings
29 | .springBeans
30 | .sts4-cache
31 | bin/
32 | !**/src/main/**/bin/
33 | !**/src/test/**/bin/
34 |
35 | ### NetBeans ###
36 | /nbproject/private/
37 | /nbbuild/
38 | /dist/
39 | /nbdist/
40 | /.nb-gradle/
41 |
42 | ### VS Code ###
43 | .vscode/
44 |
45 | ### Mac OS ###
46 | .DS_Store
47 |
--------------------------------------------------------------------------------
/proguard/HackedSelectionContainer.kt.pro:
--------------------------------------------------------------------------------
1 | -keepattributes *Annotation*,InnerClasses,EnclosingMethod,Signature,Exceptions
2 |
3 | -keep class androidx.compose.foundation.text.selection.** { *; }
4 | -keep class androidx.compose.ui.text.** { *; }
5 | -keep class androidx.compose.foundation.text.** { *; }
6 | -keep class androidx.compose.foundation.ContextMenu* { *; }
7 |
8 | -keepclassmembers class **$Companion { *; }
9 |
10 | -keep class androidx.compose.foundation.text.selection.SelectionRegistrarKt { *; }
11 | -keep class androidx.compose.foundation.text.selection.SelectionKt { *; }
12 | -keep class androidx.compose.foundation.text.selection.SelectionJvmKt { *; }
13 | -keep class androidx.compose.foundation.text.selection.Selection_desktopKt { *; }
14 | -keep class androidx.compose.foundation.text.selection.Selection { *; }
--------------------------------------------------------------------------------
/.run/SLE (Debug).run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/copyright/Server_List_Explorer_GPT_3_0.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/nbt/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | dependencies {
20 | implementation(libs.kotlinx.coroutines.core.jvm)
21 | implementation(libs.nbt)
22 | compileOnly(libs.compose.runtime)
23 | }
24 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/multiplayer/online/backend/common/IServerData.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.multiplayer.online.backend.common
20 |
21 | interface IServerData
22 |
--------------------------------------------------------------------------------
/settings/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | plugins {
20 | alias(libs.plugins.kotlin.serialization)
21 | }
22 |
23 | dependencies {
24 | implementation(libs.kotlinx.coroutines.core.jvm)
25 | implementation(libs.kotlinx.serialization.json)
26 | }
27 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/multiplayer/online/backend/common/OfflineServerData.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.multiplayer.online.backend.common
20 |
21 | data object OfflineServerData : IServerData
22 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/extensions/IntExtensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.extensions
20 |
21 | import java.util.Locale
22 |
23 | internal fun Int.toGroupedString(locale: Locale) = String.format(locale, "%,d", this)
24 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/multiplayer/online/backend/common/McSrvStatRateLimitedServerData.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.multiplayer.online.backend.common
20 |
21 | data object McSrvStatRateLimitedServerData : IServerData
22 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/common/IModuleKind.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.common
20 |
21 | sealed interface IModuleKind {
22 | data object Multiplayer : IModuleKind
23 |
24 | data object Singleplayer : IModuleKind
25 | }
26 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/util/StringExtensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.util
20 |
21 | val MinecraftColorCodeRegex = Regex("""§(?:[0-9a-fk-or]|#[0-9A-Fa-f]{6})""")
22 |
23 | fun String.stripMinecraftColorCodes() = replace(MinecraftColorCodeRegex, "")
24 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/multiplayer/online/backend/common/IServerQueryHandler.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.multiplayer.online.backend.common
20 |
21 | internal interface IServerQueryHandler {
22 | suspend fun getServerData(): IServerData
23 | }
24 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/extensions/LongExtensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.extensions
20 |
21 | internal fun Long.formatMillis() =
22 | when {
23 | this < 1000 -> "${this}ms"
24 | else -> String.format("%.1fs", this / 1000.0)
25 | }
26 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/common/IModeLoader.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.common
20 |
21 | import java.nio.file.Path
22 |
23 | internal interface IModeLoader {
24 | suspend fun autoResolvePath(): Path?
25 |
26 | suspend fun initialize(target: Path?): Result
27 | }
28 |
--------------------------------------------------------------------------------
/.run/Generate Compose Resources.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
17 |
18 |
19 | true
20 | true
21 | false
22 | false
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/test/resources/log4j2-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/navigation/Destinations.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.navigation
20 |
21 | import kotlinx.serialization.Serializable
22 |
23 | @Serializable
24 | internal object MultiplayerServerListScreen
25 |
26 | @Serializable
27 | internal object SingleplayerWorldListScreen
28 |
29 | @Serializable
30 | internal object SettingsScreen
31 |
--------------------------------------------------------------------------------
/settings/src/main/kotlin/com/spoiligaming/explorer/settings/manager/SettingsManager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.settings.manager
20 |
21 | internal interface SettingsManager {
22 | suspend fun loadSettings(): T
23 |
24 | fun saveSettings(
25 | settings: T,
26 | onComplete: (() -> Unit)? = null,
27 | )
28 |
29 | fun getCachedSettings(): T
30 | }
31 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/extensions/InputStreamExtensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.extensions
20 |
21 | import org.jetbrains.compose.resources.decodeToImageBitmap
22 | import java.io.InputStream
23 |
24 | internal fun InputStream?.asImageBitmap() = this?.readAllBytes()?.decodeToImageBitmap()
25 |
26 | internal fun InputStream.safeAsImageBitmapOrNull() = runCatching { this.use { it.asImageBitmap() } }.getOrNull()
27 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/spoiligaming/explorer/Main.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer
20 |
21 | import com.spoiligaming.explorer.ui.launchInterface
22 |
23 | fun main(args: Array) {
24 | val env = if (System.getProperty("env") == "dev") "dev" else "prod"
25 | System.setProperty("log4j2.configurationFile", "log4j2-$env.xml")
26 | System.setProperty("app.logs.dir", LogStorage.logsDir.absolutePath)
27 |
28 | ArgsParser.parse(args)
29 |
30 | launchInterface()
31 | }
32 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | rootProject.name = "server-list-explorer"
20 |
21 | pluginManagement {
22 | repositories {
23 | mavenCentral()
24 | google()
25 | gradlePluginPortal()
26 | }
27 | }
28 |
29 | dependencyResolutionManagement {
30 | repositories {
31 | google()
32 | mavenCentral()
33 | maven("https://jitpack.io/")
34 | }
35 | }
36 |
37 | include(":app")
38 | include(":core")
39 | include(":settings")
40 | include(":nbt")
41 | include(":ui")
42 |
--------------------------------------------------------------------------------
/settings/src/main/kotlin/com/spoiligaming/explorer/settings/model/SingleplayerSettings.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.settings.model
20 |
21 | import com.spoiligaming.explorer.settings.serializer.PathSerializer
22 | import kotlinx.serialization.SerialName
23 | import kotlinx.serialization.Serializable
24 | import java.nio.file.Path
25 |
26 | @Serializable
27 | data class SingleplayerSettings(
28 | @SerialName("saves_directory")
29 | @Serializable(with = PathSerializer::class)
30 | val savesDirectory: Path? = null,
31 | )
32 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/screens/multiplayer/ServerEntryMoveDirection.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | @file:OptIn(ExperimentalUuidApi::class)
20 |
21 | package com.spoiligaming.explorer.ui.screens.multiplayer
22 |
23 | import kotlin.uuid.ExperimentalUuidApi
24 | import kotlin.uuid.Uuid
25 |
26 | internal enum class ServerEntryMoveDirection {
27 | Up,
28 | Down,
29 | Left,
30 | Right,
31 | }
32 |
33 | internal data class BlockedMoveFeedback(
34 | val direction: ServerEntryMoveDirection,
35 | val targetIds: Set,
36 | val token: Long,
37 | )
38 |
--------------------------------------------------------------------------------
/settings/src/main/kotlin/com/spoiligaming/explorer/settings/model/WindowState.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.settings.model
20 |
21 | import kotlinx.serialization.SerialName
22 | import kotlinx.serialization.Serializable
23 |
24 | @Serializable
25 | data class WindowState(
26 | @SerialName("is_maximized")
27 | val isWindowMaximized: Boolean = false,
28 | @SerialName("width")
29 | val width: Int = 1024,
30 | @SerialName("height")
31 | val height: Int = 768,
32 | @SerialName("current_width")
33 | val currentWidth: Int = 1024,
34 | @SerialName("current_height")
35 | val currentHeight: Int = 768,
36 | )
37 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/singleplayer/SingleplayerModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.singleplayer
20 |
21 | import com.spoiligaming.explorer.minecraft.common.IModeLoader
22 | import kotlinx.coroutines.Dispatchers
23 | import kotlinx.coroutines.withContext
24 | import java.nio.file.Path
25 |
26 | // TODO
27 | object SingleplayerModule : IModeLoader {
28 | override suspend fun autoResolvePath(): Path? =
29 | withContext(Dispatchers.Default) {
30 | WorldDirectoryPaths.findDefault()
31 | }
32 |
33 | override suspend fun initialize(target: Path?) = TODO()
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/spoiligaming/explorer/ArgsParser.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer
20 |
21 | import io.github.oshai.kotlinlogging.KotlinLogging
22 | import org.apache.logging.log4j.Level
23 | import org.apache.logging.log4j.core.config.Configurator
24 |
25 | internal object ArgsParser {
26 | fun parse(args: Array) {
27 | if ("--verbose" in args) {
28 | enableVerboseLogging()
29 | }
30 | }
31 |
32 | private fun enableVerboseLogging() {
33 | Configurator.setRootLevel(Level.ALL)
34 | logger.info { "Verbose logging enabled." }
35 | }
36 | }
37 |
38 | private val logger = KotlinLogging.logger {}
39 |
--------------------------------------------------------------------------------
/core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | plugins {
20 | alias(libs.plugins.kotlin.serialization)
21 | }
22 |
23 | dependencies {
24 | implementation(project(":settings"))
25 | implementation(project(":nbt"))
26 |
27 | implementation(libs.kotlinx.coroutines.core.jvm)
28 | implementation(libs.kotlinx.serialization.json)
29 |
30 | implementation(libs.mcUtils)
31 |
32 | implementation(libs.ktor.client.core.jvm)
33 | implementation(libs.ktor.client.cio.jvm)
34 | testImplementation(libs.ktor.client.core.jvm)
35 | testImplementation(libs.ktor.client.cio.jvm)
36 | testImplementation(libs.ktor.client.mock.jvm)
37 |
38 | implementation(libs.oshi.core)
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/resources/log4j2-prod.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
15 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/multiplayer/online/backend/common/OnlineServerDataResourceResult.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.multiplayer.online.backend.common
20 |
21 | sealed class OnlineServerDataResourceResult {
22 | object Loading : OnlineServerDataResourceResult()
23 |
24 | data class Success(
25 | val data: T,
26 | ) : OnlineServerDataResourceResult()
27 |
28 | data class Error(
29 | val throwable: Throwable,
30 | ) : OnlineServerDataResourceResult()
31 |
32 | data class RateLimited(
33 | val data: McSrvStatRateLimitedServerData,
34 | ) : OnlineServerDataResourceResult()
35 | }
36 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.theme
20 |
21 | import androidx.compose.material3.ColorScheme
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.ui.graphics.Color
24 |
25 | internal val SecureChatLight = Color(0xFF1565C0) // blue 800
26 | internal val SecureChatDark = Color(0xFF64B5F6) // blue 300
27 |
28 | internal val LinkLight = Color(0xFF2F6FEB)
29 | internal val LinkDark = Color(0xFF6EA8FE)
30 |
31 | @Suppress("UnusedReceiverParameter")
32 | internal val ColorScheme.secureChatTint
33 | @Composable
34 | get() = if (LocalDarkTheme.current) SecureChatDark else SecureChatLight
35 |
36 | @Suppress("UnusedReceiverParameter")
37 | internal val ColorScheme.linkTint
38 | @Composable
39 | get() = if (LocalDarkTheme.current) LinkDark else LinkLight
40 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/extensions/ColorExtensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.extensions
20 |
21 | import androidx.compose.ui.graphics.toArgb
22 | import androidx.compose.ui.graphics.Color as ComposeColor
23 | import java.awt.Color as AwtColor
24 |
25 | internal fun ComposeColor.toAwtColor(): AwtColor {
26 | val argb = toArgb()
27 | return AwtColor(
28 | (argb shr 16) and 0xFF,
29 | (argb shr 8) and 0xFF,
30 | argb and 0xFF,
31 | (argb shr 24) and 0xFF,
32 | )
33 | }
34 |
35 | internal fun AwtColor.toComposeColor() =
36 | ComposeColor(
37 | red = red / 255f,
38 | green = green / 255f,
39 | blue = blue / 255f,
40 | alpha = alpha / 255f,
41 | )
42 |
43 | internal fun ComposeColor.toHex() =
44 | String.format("%06X", toArgb() and 0x00FFFFFF) + String.format("%02X", (alpha * 255).toInt())
45 |
--------------------------------------------------------------------------------
/ui/src/main/composeResources/drawable/flag_uk.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/settings/src/main/kotlin/com/spoiligaming/explorer/settings/serializer/LocaleSerializer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.settings.serializer
20 |
21 | import kotlinx.serialization.KSerializer
22 | import kotlinx.serialization.descriptors.PrimitiveKind
23 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
24 | import kotlinx.serialization.descriptors.SerialDescriptor
25 | import kotlinx.serialization.encoding.Decoder
26 | import kotlinx.serialization.encoding.Encoder
27 | import java.util.Locale
28 |
29 | object LocaleSerializer : KSerializer {
30 | override val descriptor: SerialDescriptor =
31 | PrimitiveSerialDescriptor("Locale", PrimitiveKind.STRING)
32 |
33 | override fun serialize(
34 | encoder: Encoder,
35 | value: Locale,
36 | ) = encoder.encodeString(value.toLanguageTag())
37 |
38 | override fun deserialize(decoder: Decoder): Locale = Locale.forLanguageTag(decoder.decodeString())
39 | }
40 |
--------------------------------------------------------------------------------
/settings/src/main/kotlin/com/spoiligaming/explorer/settings/serializer/PathSerializer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.settings.serializer
20 |
21 | import kotlinx.serialization.KSerializer
22 | import kotlinx.serialization.descriptors.PrimitiveKind
23 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
24 | import kotlinx.serialization.descriptors.SerialDescriptor
25 | import kotlinx.serialization.encoding.Decoder
26 | import kotlinx.serialization.encoding.Encoder
27 | import java.nio.file.Path
28 | import java.nio.file.Paths
29 |
30 | object PathSerializer : KSerializer {
31 | override val descriptor: SerialDescriptor =
32 | PrimitiveSerialDescriptor("Path", PrimitiveKind.STRING)
33 |
34 | override fun serialize(
35 | encoder: Encoder,
36 | value: Path,
37 | ) {
38 | encoder.encodeString(value.toString())
39 | }
40 |
41 | override fun deserialize(decoder: Decoder): Path = Paths.get(decoder.decodeString())
42 | }
43 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/widgets/ItemSelectableDropdownMenu.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.widgets
20 |
21 | import androidx.compose.foundation.layout.fillMaxWidth
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.ui.Modifier
24 |
25 | @Composable
26 | internal fun ItemSelectableDropdownMenu(
27 | title: String,
28 | description: String,
29 | note: String? = null,
30 | conflictReason: String? = null,
31 | selectedOption: DropdownOption,
32 | options: List,
33 | onOptionSelected: (DropdownOption) -> Unit,
34 | ) = SettingTile(
35 | title = title,
36 | description = description,
37 | note = note,
38 | conflictReason = conflictReason,
39 | trailingContent = {
40 | SelectableDropdown(
41 | selectedOption,
42 | options,
43 | onOptionSelected,
44 | modifier = Modifier.fillMaxWidth(DROPDOWN_WIDTH_RATIO),
45 | )
46 | },
47 | )
48 |
49 | private const val DROPDOWN_WIDTH_RATIO = 0.25f
50 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/common/PathResolver.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.common
20 |
21 | import java.nio.file.Files
22 | import java.nio.file.Path
23 |
24 | internal interface PathResolver {
25 | fun find(path: String): Path?
26 |
27 | fun findDefault(): Path?
28 |
29 | fun validate(path: Path?)
30 | }
31 |
32 | internal fun PathResolver.requireExistingDirectory(path: Path?) {
33 | requireNotNull(path) { "Path must not be null." }
34 | require(Files.exists(path)) { "Path does not exist: $path" }
35 | require(Files.isDirectory(path)) { "Path must be a directory: $path" }
36 | require(Files.isReadable(path)) { "Path is not readable: $path" }
37 | }
38 |
39 | internal fun PathResolver.requireExistingFile(path: Path?) {
40 | requireNotNull(path) { "File path must not be null." }
41 | require(Files.exists(path)) { "File path does not exist: $path" }
42 | require(Files.isRegularFile(path)) { "Path must be a file: $path" }
43 | require(Files.isReadable(path)) { "File is not readable: $path" }
44 | }
45 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/util/FilePickerUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.util
20 |
21 | import androidx.compose.runtime.Composable
22 | import io.github.vinceglb.filekit.dialogs.FileKitMode
23 | import io.github.vinceglb.filekit.dialogs.FileKitType
24 | import io.github.vinceglb.filekit.dialogs.compose.rememberDirectoryPickerLauncher
25 | import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher
26 | import java.nio.file.Path
27 |
28 | @Composable
29 | internal fun rememberWorldSavesPickerLauncher(
30 | title: String,
31 | onDirectory: (Path) -> Unit,
32 | ) = rememberDirectoryPickerLauncher(
33 | title = title,
34 | ) { result ->
35 | result?.file?.toPath()?.let(onDirectory)
36 | }
37 |
38 | @Composable
39 | internal fun rememberServerListFilePickerLauncher(
40 | title: String,
41 | onFile: (Path) -> Unit,
42 | ) = rememberFilePickerLauncher(
43 | type = FileKitType.File(extensions = listOf("dat")),
44 | mode = FileKitMode.Single,
45 | title = title,
46 | ) { result ->
47 | result?.file?.toPath()?.let(onFile)
48 | }
49 |
--------------------------------------------------------------------------------
/.github/workflows/verify-pr.yml:
--------------------------------------------------------------------------------
1 | name: Build and Verify
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 | types: [ opened, synchronize, reopened, ready_for_review ]
9 |
10 | permissions:
11 | contents: read
12 |
13 | jobs:
14 | verify-pr:
15 | name: Verify with Gradle build
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - name: Install Node 24
20 | if: env.ACT == 'true'
21 | run: |
22 | curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
23 | apt-get install -y nodejs
24 | node -v
25 |
26 | - name: Checkout
27 | uses: actions/checkout@v6
28 |
29 | - name: Set up JDK 25
30 | uses: actions/setup-java@v5
31 | with:
32 | distribution: graalvm
33 | java-version: 25
34 |
35 | - name: Setup Gradle
36 | uses: gradle/actions/setup-gradle@v5
37 | with:
38 | gradle-version: wrapper
39 | cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
40 | cache-read-only: false
41 |
42 | - name: Normalize gradlew line endings
43 | if: env.ACT == 'true'
44 | run: |
45 | # remove CR at end-of-line
46 | sed -i 's/\r$//' ./gradlew
47 | shell: bash
48 |
49 | - name: Make gradlew executable
50 | run: chmod +x gradlew
51 |
52 | - name: Generate license report
53 | run: ./gradlew generateLicenseReport --no-parallel --no-configuration-cache
54 |
55 | - name: Gradle build
56 | run: ./gradlew app:buildWithProjectModules
57 |
58 | - name: Upload reports
59 | if: ${{ always() && !env.ACT }}
60 | uses: actions/upload-artifact@v6
61 | with:
62 | name: pr-verification-reports
63 | path: |
64 | **/build/reports/ktlint/**
65 | **/build/reports/**/ktlint*.html
66 | **/build/reports/tests/**
67 | **/build/test-results/**
68 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/screens/multiplayer/querymethod/QueryMethodModels.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.screens.multiplayer.querymethod
20 |
21 | import com.spoiligaming.explorer.settings.model.ServerQueryMethod
22 |
23 | internal data class QueryMethodDefinition(
24 | val method: ServerQueryMethod,
25 | val title: String,
26 | val chips: List,
27 | val configuration: QueryMethodConfiguration? = null,
28 | )
29 |
30 | internal data class QueryMethodConfiguration(
31 | val title: String,
32 | val items: List,
33 | )
34 |
35 | internal sealed interface QueryMethodSettingItem
36 |
37 | internal data class TimeoutSliderSpec(
38 | val title: String,
39 | val description: String?,
40 | val valueSeconds: Int,
41 | val valueRangeSeconds: ClosedFloatingPointRange,
42 | val onValueChangeSeconds: (Int) -> Unit,
43 | ) : QueryMethodSettingItem
44 |
45 | internal data class QueryMethodCheckboxSpec(
46 | val title: String,
47 | val description: String?,
48 | val checked: Boolean,
49 | val onCheckedChange: (Boolean) -> Unit,
50 | ) : QueryMethodSettingItem
51 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/common/UnifiedModeLoader.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.common
20 |
21 | import com.spoiligaming.explorer.minecraft.multiplayer.ServerListModule
22 | import com.spoiligaming.explorer.minecraft.singleplayer.SingleplayerModule
23 | import kotlinx.coroutines.Dispatchers
24 | import kotlinx.coroutines.withContext
25 | import java.nio.file.Path
26 |
27 | object UnifiedModeInitializer {
28 | suspend fun autoDetect(mode: IModuleKind) =
29 | withContext(Dispatchers.Default) {
30 | when (mode) {
31 | IModuleKind.Multiplayer -> ServerListModule.autoResolvePath()
32 | IModuleKind.Singleplayer -> SingleplayerModule.autoResolvePath()
33 | }
34 | }
35 |
36 | @Suppress("UNCHECKED_CAST")
37 | suspend fun initialize(
38 | mode: IModuleKind,
39 | target: Path?,
40 | ) = withContext(Dispatchers.Default) {
41 | when (mode) {
42 | IModuleKind.Multiplayer -> ServerListModule.initialize(target) as Result
43 | IModuleKind.Singleplayer -> SingleplayerModule.initialize(target) as Result
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/resources/log4j2-dev.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
22 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/widgets/SlickTextButton.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.widgets
20 |
21 | import androidx.compose.material3.ButtonDefaults
22 | import androidx.compose.material3.ListItemDefaults
23 | import androidx.compose.material3.MaterialTheme
24 | import androidx.compose.material3.TextButton
25 | import androidx.compose.runtime.Composable
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.graphics.Color
28 | import androidx.compose.ui.input.pointer.PointerIcon
29 | import androidx.compose.ui.input.pointer.pointerHoverIcon
30 |
31 | @Composable
32 | internal fun SlickTextButton(
33 | contentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
34 | onClick: () -> Unit,
35 | modifier: Modifier,
36 | enabled: Boolean = true,
37 | content: @Composable () -> Unit,
38 | ) = TextButton(
39 | onClick = onClick,
40 | modifier = modifier.pointerHoverIcon(PointerIcon.Hand),
41 | enabled = enabled,
42 | shape = MaterialTheme.shapes.small,
43 | colors =
44 | ButtonDefaults.textButtonColors(
45 | contentColor = contentColor,
46 | disabledContentColor = ListItemDefaults.colors().supportingTextColor,
47 | ),
48 | ) {
49 | content()
50 | }
51 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/multiplayer/online/backend/common/OnlineServerDataModels.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.multiplayer.online.backend.common
20 |
21 | sealed interface OnlineServerData : IServerData {
22 | val motd: String
23 | val onlinePlayers: Int
24 | val maxPlayers: Int
25 | val icon: String
26 | val versionInfo: String
27 | val protocolVersion: Int
28 | val ip: String
29 | val info: String?
30 | }
31 |
32 | data class McSrvStatOnlineServerData(
33 | override val motd: String,
34 | override val onlinePlayers: Int,
35 | override val maxPlayers: Int,
36 | override val icon: String,
37 | override val versionInfo: String,
38 | override val protocolVersion: Int,
39 | override val ip: String,
40 | override val info: String?,
41 | val versionName: String?,
42 | val eulaBlocked: Boolean,
43 | ) : OnlineServerData
44 |
45 | data class McUtilsOnlineServerData(
46 | override val motd: String,
47 | override val onlinePlayers: Int,
48 | override val maxPlayers: Int,
49 | override val icon: String,
50 | override val versionInfo: String,
51 | override val protocolVersion: Int,
52 | override val ip: String,
53 | override val info: String?,
54 | val ping: Long,
55 | val secureChatEnforced: Boolean,
56 | ) : OnlineServerData
57 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/widgets/ItemSwitch.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.widgets
20 |
21 | import androidx.compose.material3.Switch
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.ui.Modifier
24 | import androidx.compose.ui.input.pointer.PointerIcon
25 | import androidx.compose.ui.input.pointer.pointerHoverIcon
26 |
27 | @Composable
28 | internal fun ItemSwitch(
29 | isChecked: Boolean,
30 | onCheckedChange: (Boolean) -> Unit,
31 | enabled: Boolean,
32 | ) = Switch(
33 | modifier =
34 | Modifier.pointerHoverIcon(
35 | icon = if (enabled) PointerIcon.Hand else PointerIcon.Default,
36 | ),
37 | checked = isChecked,
38 | onCheckedChange = onCheckedChange,
39 | enabled = enabled,
40 | )
41 |
42 | @Composable
43 | internal fun ItemSwitch(
44 | title: String,
45 | description: String,
46 | note: String? = null,
47 | conflictReason: String? = null,
48 | isChecked: Boolean,
49 | onCheckedChange: (Boolean) -> Unit,
50 | ) = SettingTile(
51 | title = title,
52 | description = description,
53 | note = note,
54 | conflictReason = conflictReason,
55 | trailingContent = {
56 | ItemSwitch(
57 | isChecked = isChecked,
58 | onCheckedChange = onCheckedChange,
59 | enabled = conflictReason == null,
60 | )
61 | },
62 | )
63 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/singleplayer/WorldDirectoryPaths.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.singleplayer
20 |
21 | import com.spoiligaming.explorer.minecraft.common.PathResolver
22 | import com.spoiligaming.explorer.minecraft.common.requireExistingDirectory
23 | import com.spoiligaming.explorer.util.OSUtils
24 | import java.nio.file.Files
25 | import java.nio.file.Path
26 | import java.nio.file.Paths
27 |
28 | object WorldDirectoryPaths : PathResolver {
29 | override fun find(path: String) = Paths.get(path).takeIf { Files.exists(it) }
30 |
31 | override fun findDefault(): Path? {
32 | val home = System.getProperty("user.home") ?: return null
33 | val p =
34 | when {
35 | OSUtils.isWindows -> Paths.get(home, "AppData", "Roaming", ".minecraft", "saves")
36 | OSUtils.isMacOS ->
37 | Paths.get(
38 | home,
39 | "Library",
40 | "Application Support",
41 | "minecraft",
42 | "saves",
43 | )
44 |
45 | OSUtils.isLinux -> Paths.get(home, ".minecraft", "saves")
46 | else -> return null
47 | }
48 | return p.takeIf(Files::exists)
49 | }
50 |
51 | override fun validate(path: Path?) = requireExistingDirectory(path)
52 | }
53 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/multiplayer/ServerListModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.multiplayer
20 |
21 | import com.spoiligaming.explorer.minecraft.common.IModeLoader
22 | import com.spoiligaming.explorer.multiplayer.repository.ServerListRepository
23 | import io.github.oshai.kotlinlogging.KotlinLogging
24 | import kotlinx.coroutines.Dispatchers
25 | import kotlinx.coroutines.withContext
26 | import java.nio.file.Path
27 |
28 | object ServerListModule : IModeLoader {
29 | var repository: ServerListRepository? = null
30 | private set
31 |
32 | override suspend fun autoResolvePath() =
33 | withContext(Dispatchers.Default) {
34 | ServerListPaths.findDefault()
35 | }
36 |
37 | override suspend fun initialize(target: Path?) =
38 | withContext(Dispatchers.Default) {
39 | runCatching {
40 | ServerListPaths.validate(target)
41 | val p = requireNotNull(target) { "target must not be null after validation" }
42 |
43 | ServerListRepository(p).apply {
44 | load()
45 | repository = this
46 | }
47 | }.onFailure { e ->
48 | logger.error(e) { "Validation failed for server list path: $target" }
49 | }
50 | }
51 | }
52 |
53 | private val logger = KotlinLogging.logger {}
54 |
--------------------------------------------------------------------------------
/settings/src/main/kotlin/com/spoiligaming/explorer/settings/model/ThemeSettings.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.settings.model
20 |
21 | import kotlinx.serialization.SerialName
22 | import kotlinx.serialization.Serializable
23 |
24 | @Serializable
25 | enum class ThemeMode {
26 | @SerialName("light")
27 | Light,
28 |
29 | @SerialName("dark")
30 | Dark,
31 |
32 | @SerialName("system")
33 | System,
34 | }
35 |
36 | @Serializable
37 | enum class ThemePaletteStyle {
38 | TonalSpot,
39 | Neutral,
40 | Vibrant,
41 | Expressive,
42 | Rainbow,
43 | FruitSalad,
44 | Monochrome,
45 | Fidelity,
46 | Content,
47 | }
48 |
49 | @Serializable
50 | data class ThemeSettings(
51 | @SerialName("theme_mode")
52 | val themeMode: ThemeMode = ThemeMode.System,
53 | @SerialName("seed_color")
54 | val seedColor: String = DEFAULT_SEED_COLOR,
55 | @SerialName("palette_style")
56 | val paletteStyle: ThemePaletteStyle = ThemePaletteStyle.TonalSpot,
57 | @SerialName("contrast_level")
58 | val contrastLevel: Double = DEFAULT_CONTRAST_LEVEL,
59 | @SerialName("use_system_accent_color")
60 | val useSystemAccentColor: Boolean = false,
61 | @SerialName("amoled_mode")
62 | val amoledMode: Boolean = false,
63 | ) {
64 | companion object {
65 | const val DEFAULT_SEED_COLOR = "#8D15FFED"
66 | const val DEFAULT_CONTRAST_LEVEL = 0.0
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/extensions/StringExtensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.extensions
20 |
21 | import androidx.compose.ui.graphics.Color
22 |
23 | internal fun String.toComposeColor(): Color {
24 | require(startsWith("#")) { "Color must start with '#'" }
25 | require(length == 7 || length == 9) {
26 | "Invalid color length: must be 7 (#RRGGBB) or 9 (#AARRGGBB) characters"
27 | }
28 |
29 | val hex = substring(1)
30 | val (alphaHex, colorHex) =
31 | when (hex.length) {
32 | 6 -> null to hex
33 | 8 -> hex.take(2) to hex.drop(2)
34 | else -> throw IllegalArgumentException("Invalid hex length: ${hex.length} after '#'")
35 | }
36 |
37 | fun parseHexComponent(
38 | hex: String,
39 | component: String,
40 | ): Int {
41 | require(hex.length == 2) { "Invalid $component hex length" }
42 | return hex.toIntOrNull(16)?.takeIf { it in 0..255 }
43 | ?: throw IllegalArgumentException("Invalid $component hex value: '$hex'")
44 | }
45 |
46 | val alpha = alphaHex?.let { parseHexComponent(it, "alpha") } ?: 255
47 | val red = parseHexComponent(colorHex.substring(0, 2), "red")
48 | val green = parseHexComponent(colorHex.substring(2, 4), "green")
49 | val blue = parseHexComponent(colorHex.substring(4, 6), "blue")
50 |
51 | return Color(alpha, red, green, blue)
52 | }
53 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/multiplayer/online/backend/mcsrvstat/OnlineServerDataDeserializer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.multiplayer.online.backend.mcsrvstat
20 |
21 | import com.spoiligaming.explorer.minecraft.multiplayer.online.backend.common.McSrvStatOnlineServerData
22 | import kotlinx.serialization.json.Json
23 |
24 | internal object OnlineServerDataSerializer {
25 | private val json by lazy {
26 | Json {
27 | ignoreUnknownKeys = true
28 | }
29 | }
30 |
31 | fun decode(rawJson: String) = json.decodeFromString(McSrvStatServerStatusResponse.serializer(), rawJson)
32 |
33 | fun reduce(response: McSrvStatServerStatusResponse) =
34 | McSrvStatOnlineServerData(
35 | motd = response.motd.raw.joinToString(separator = "\n"),
36 | onlinePlayers = response.players.online.toInt(),
37 | maxPlayers = response.players.max.toInt(),
38 | versionInfo = response.versionInfo,
39 | icon = response.icon,
40 | versionName = response.protocol.name,
41 | protocolVersion = response.protocol.version.toInt(),
42 | ip = response.ip,
43 | info = response.info?.raw?.joinToString(separator = "\n"),
44 | eulaBlocked = response.eulaBlocked,
45 | )
46 |
47 | // just in case
48 | fun fromJson(rawJson: String) = reduce(decode(rawJson))
49 | }
50 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/util/DurationFormatting.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.util
20 |
21 | import kotlin.time.Duration.Companion.days
22 | import kotlin.time.Duration.Companion.hours
23 | import kotlin.time.Duration.Companion.milliseconds
24 | import kotlin.time.Duration.Companion.minutes
25 | import kotlin.time.Duration.Companion.seconds
26 | import kotlin.time.DurationUnit
27 |
28 | fun Long.toHumanReadableDuration(): String {
29 | val d = this.milliseconds
30 |
31 | return when {
32 | d < 1.seconds ->
33 | "${d.inWholeMilliseconds} ms"
34 |
35 | d < 1.minutes -> {
36 | val sec = d.toDouble(DurationUnit.SECONDS)
37 | String.format("%.1f seconds", sec)
38 | }
39 |
40 | d < 1.hours -> {
41 | val m = d.inWholeMinutes
42 | "$m ${plural(m, "minute")}"
43 | }
44 |
45 | d < 1.days -> {
46 | val h = d.inWholeHours
47 | "$h ${plural(h, "hour")}"
48 | }
49 |
50 | d < 365.days -> {
51 | val dayCount = d.inWholeDays
52 | "$dayCount ${plural(dayCount, "day")}"
53 | }
54 |
55 | else -> {
56 | val years = d.inWholeDays / 365
57 | "$years ${plural(years, "year")}"
58 | }
59 | }
60 | }
61 |
62 | private fun plural(
63 | value: Long,
64 | unit: String,
65 | ) = if (value == 1L) unit else "${unit}s"
66 |
--------------------------------------------------------------------------------
/settings/src/main/kotlin/com/spoiligaming/explorer/settings/model/MultiplayerSettings.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.settings.model
20 |
21 | import com.spoiligaming.explorer.settings.serializer.PathSerializer
22 | import kotlinx.serialization.SerialName
23 | import kotlinx.serialization.Serializable
24 | import java.nio.file.Path
25 |
26 | @Serializable
27 | enum class ActionBarOrientation {
28 | @SerialName("right")
29 | Right,
30 |
31 | @SerialName("top")
32 | Top,
33 |
34 | @SerialName("left")
35 | Left,
36 |
37 | @SerialName("bottom")
38 | Bottom,
39 | }
40 |
41 | @Serializable
42 | enum class ServerQueryMethod {
43 | @SerialName("mcsrvstat")
44 | McSrvStat,
45 |
46 | @SerialName("mcutils")
47 | McUtils,
48 | }
49 |
50 | @Serializable
51 | data class MultiplayerSettings(
52 | @SerialName("server_list_file")
53 | @Serializable(with = PathSerializer::class)
54 | val serverListFile: Path? = null,
55 | @SerialName("server_query_method")
56 | val serverQueryMethod: ServerQueryMethod = ServerQueryMethod.McUtils,
57 | @SerialName("entry_size_scale")
58 | val serverEntryScale: Float = 1.0f,
59 | @SerialName("drag_shake_intensity_deg")
60 | val dragShakeIntensityDegrees: Int = 1,
61 | @SerialName("action_bar_orientation")
62 | val actionBarOrientation: ActionBarOrientation = ActionBarOrientation.Right,
63 | // TODO: auto refresh icons on load/reload
64 | )
65 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/extensions/ImageBitmapExtensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.extensions
20 |
21 | import androidx.compose.ui.graphics.ImageBitmap
22 | import androidx.compose.ui.graphics.toAwtImage
23 | import io.github.oshai.kotlinlogging.KotlinLogging
24 | import java.io.ByteArrayInputStream
25 | import java.io.ByteArrayOutputStream
26 | import javax.imageio.ImageIO
27 | import kotlin.io.encoding.Base64
28 |
29 | private fun ImageBitmap.toPngBytes(): ByteArray {
30 | require(width > 0 && height > 0) {
31 | "ImageBitmap must have positive dimensions, but was ${width}x$height"
32 | }
33 | return runCatching {
34 | ByteArrayOutputStream().use { baos ->
35 | val success = ImageIO.write(toAwtImage(), "png", baos)
36 | check(success) { "ImageIO.write returned false while encoding PNG" }
37 | baos.toByteArray()
38 | }
39 | }.getOrElse { e ->
40 | logger.error(e) { "Failed to encode ImageBitmap to PNG" }
41 | throw e
42 | }
43 | }
44 |
45 | fun ImageBitmap.toPngInputStream() =
46 | runCatching {
47 | ByteArrayInputStream(toPngBytes())
48 | }.getOrElse { e ->
49 | logger.error(e) { "Failed to create InputStream from PNG bytes" }
50 | throw e
51 | }
52 |
53 | fun ImageBitmap.toPngBase64() =
54 | runCatching {
55 | Base64.encode(toPngBytes())
56 | }.getOrElse { e ->
57 | logger.error(e) { "Failed to Base64-encode PNG bytes" }
58 | throw e
59 | }
60 |
61 | private val logger = KotlinLogging.logger {}
62 |
--------------------------------------------------------------------------------
/proguard/kotlinx-serialization.pro:
--------------------------------------------------------------------------------
1 | # --- Source: https://github.com/Kotlin/kotlinx.serialization/blob/master/rules/common.pro
2 |
3 | # Keep `Companion` object fields of serializable classes.
4 | # This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
5 | -if @kotlinx.serialization.Serializable class **
6 | -keepclassmembers class <1> {
7 | static <1>$* Companion;
8 | }
9 |
10 | # Keep names for named companion object from obfuscation
11 | # Names of a class and of a field are important in lookup of named companion in runtime
12 | -keepnames @kotlinx.serialization.internal.NamedCompanion class *
13 | -if @kotlinx.serialization.internal.NamedCompanion class *
14 | -keepclassmembernames class * {
15 | static <1> *;
16 | }
17 |
18 | # Keep `serializer()` on companion objects (both default and named) of serializable classes.
19 | -if @kotlinx.serialization.Serializable class ** {
20 | static **$* *;
21 | }
22 | -keepclassmembers class <2>$<3> {
23 | kotlinx.serialization.KSerializer serializer(...);
24 | }
25 |
26 | # Keep `INSTANCE.serializer()` of serializable objects.
27 | -if @kotlinx.serialization.Serializable class ** {
28 | public static ** INSTANCE;
29 | }
30 | -keepclassmembers class <1> {
31 | public static <1> INSTANCE;
32 | kotlinx.serialization.KSerializer serializer(...);
33 | }
34 |
35 | # @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
36 | -keepattributes RuntimeVisibleAnnotations,AnnotationDefault
37 |
38 | # Don't print notes about potential mistakes or omissions in the configuration for kotlinx-serialization classes
39 | # See also https://github.com/Kotlin/kotlinx.serialization/issues/1900
40 | -dontnote kotlinx.serialization.**
41 |
42 | # Serialization core uses `java.lang.ClassValue` for caching inside these specified classes.
43 | # If there is no `java.lang.ClassValue` (for example, in Android), then R8/ProGuard will print a warning.
44 | # However, since in this case they will not be used, we can disable these warnings
45 | -dontwarn kotlinx.serialization.internal.ClassValueReferences
46 |
47 | # disable optimisation for descriptor field because in some versions of ProGuard, optimization generates incorrect bytecode that causes a verification error
48 | # see https://github.com/Kotlin/kotlinx.serialization/issues/2719
49 | -keepclassmembers public class **$$serializer {
50 | private ** descriptor;
51 | }
52 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/multiplayer/ServerListPaths.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.multiplayer
20 |
21 | import com.spoiligaming.explorer.minecraft.common.PathResolver
22 | import com.spoiligaming.explorer.minecraft.common.requireExistingFile
23 | import com.spoiligaming.explorer.util.OSUtils
24 | import java.nio.file.Files
25 | import java.nio.file.Path
26 | import java.nio.file.Paths
27 |
28 | object ServerListPaths : PathResolver {
29 | override fun find(path: String) = Paths.get(path).takeIf { Files.exists(it) }
30 |
31 | override fun findDefault(): Path? {
32 | val home = System.getProperty("user.home") ?: return null
33 | val p =
34 | when {
35 | OSUtils.isWindows ->
36 | Paths.get(
37 | home,
38 | "AppData",
39 | "Roaming",
40 | ".minecraft",
41 | "servers.dat",
42 | )
43 | OSUtils.isMacOS ->
44 | Paths.get(
45 | home,
46 | "Library",
47 | "Application Support",
48 | "minecraft",
49 | "servers.dat",
50 | )
51 |
52 | OSUtils.isLinux -> Paths.get(home, ".minecraft", "servers.dat")
53 | else -> return null
54 | }
55 | return p.takeIf(Files::exists)
56 | }
57 |
58 | override fun validate(path: Path?) = requireExistingFile(path)
59 | }
60 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/snackbar/SnackbarHostManager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.snackbar
20 |
21 | import androidx.compose.material3.SnackbarHost
22 | import androidx.compose.material3.SnackbarHostState
23 | import androidx.compose.material3.SnackbarResult
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.runtime.LaunchedEffect
26 | import androidx.compose.runtime.remember
27 | import androidx.compose.runtime.rememberCoroutineScope
28 | import androidx.compose.ui.Modifier
29 | import kotlinx.coroutines.launch
30 |
31 | @Composable
32 | internal fun SnackbarHostManager(modifier: Modifier = Modifier) {
33 | val snackbarHostState = remember { SnackbarHostState() }
34 | val scope = rememberCoroutineScope()
35 | val eventsFlow = SnackbarController.events
36 |
37 | LaunchedEffect(eventsFlow, snackbarHostState) {
38 | scope.launch {
39 | eventsFlow.collect { event ->
40 | snackbarHostState.currentSnackbarData?.dismiss()
41 |
42 | val result =
43 | snackbarHostState.showSnackbar(
44 | message = event.message,
45 | actionLabel = event.action?.name,
46 | duration = event.duration,
47 | )
48 |
49 | if (result == SnackbarResult.ActionPerformed) {
50 | event.action?.action?.invoke()
51 | }
52 | }
53 | }
54 | }
55 |
56 | SnackbarHost(
57 | hostState = snackbarHostState,
58 | modifier = modifier,
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/spoiligaming/explorer/LogStorage.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer
20 |
21 | import com.spoiligaming.explorer.build.BuildConfig
22 | import java.io.File
23 |
24 | internal object LogStorage {
25 | private const val APP_DIR = "ServerListExplorer"
26 |
27 | private val isPortableWindows
28 | get() = isWindows && BuildConfig.DISTRIBUTION.contains("portable", ignoreCase = true)
29 |
30 | private val osName
31 | get() = System.getProperty("os.name")?.lowercase().orEmpty()
32 |
33 | private val isWindows
34 | get() = osName.contains("win")
35 |
36 | private val isMac
37 | get() = osName.contains("mac")
38 |
39 | val logsDir
40 | get() =
41 | if (isPortableWindows) {
42 | File("logs")
43 | } else {
44 | when {
45 | isWindows -> windowsLogs()
46 | isMac -> macLogs()
47 | else -> linuxLogs()
48 | }
49 | }
50 |
51 | private fun windowsLogs(): File {
52 | val local = System.getenv("LOCALAPPDATA")?.takeIf { it.isNotBlank() }
53 | val base =
54 | local?.let { File(it, APP_DIR) }
55 | ?: File(System.getProperty("user.home"), "AppData/Local/$APP_DIR")
56 | return base.resolve("logs")
57 | }
58 |
59 | private fun macLogs() = File(System.getProperty("user.home"), "Library/Logs/$APP_DIR")
60 |
61 | private fun linuxLogs(): File {
62 | val state = System.getenv("XDG_STATE_HOME")?.takeIf { it.isNotBlank() }
63 | val base =
64 | state?.let { File(it, APP_DIR) }
65 | ?: File(System.getProperty("user.home"), ".local/state/$APP_DIR")
66 | return base.resolve("logs")
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/screens/multiplayer/ShimmerServerEntry.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | @file:OptIn(ExperimentalUuidApi::class)
20 |
21 | package com.spoiligaming.explorer.ui.screens.multiplayer
22 |
23 | import androidx.compose.foundation.BorderStroke
24 | import androidx.compose.foundation.border
25 | import androidx.compose.material3.CardDefaults
26 | import androidx.compose.material3.ElevatedCard
27 | import androidx.compose.runtime.Composable
28 | import androidx.compose.ui.Modifier
29 | import androidx.compose.ui.graphics.Color
30 | import androidx.compose.ui.unit.dp
31 | import com.spoiligaming.explorer.ui.com.spoiligaming.explorer.ui.LocalAmoledActive
32 | import com.valentinilk.shimmer.Shimmer
33 | import com.valentinilk.shimmer.shimmer
34 | import kotlin.uuid.ExperimentalUuidApi
35 |
36 | // TODO
37 | @Composable
38 | internal fun ShimmerServerEntry(
39 | modifier: Modifier = Modifier,
40 | shimmer: Shimmer,
41 | ) {
42 | val amoledOn = LocalAmoledActive.current
43 |
44 | ElevatedCard(
45 | modifier =
46 | modifier
47 | .shimmer(shimmer)
48 | .border(
49 | border =
50 | if (amoledOn) {
51 | CardDefaults.outlinedCardBorder()
52 | } else {
53 | BorderStroke(
54 | 0.dp,
55 | Color.Transparent,
56 | )
57 | },
58 | shape = CardDefaults.shape,
59 | ),
60 | colors =
61 | CardDefaults
62 | .cardColors()
63 | .copy(
64 | containerColor = if (amoledOn) Color.Black else CardDefaults.cardColors().containerColor,
65 | ),
66 | ) {}
67 | }
68 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/Localization.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui
20 |
21 | import androidx.compose.runtime.Composable
22 | import androidx.compose.runtime.CompositionLocalProvider
23 | import androidx.compose.runtime.ProvidedValue
24 | import androidx.compose.runtime.staticCompositionLocalOf
25 | import com.spoiligaming.explorer.ui.com.spoiligaming.explorer.ui.LocalPrefs
26 | import org.jetbrains.compose.resources.StringResource
27 | import org.jetbrains.compose.resources.stringResource
28 | import java.util.Locale
29 |
30 | private object AppLocale {
31 | private var initialDefaultLocale: Locale? = null
32 | private val LocalLocaleTag = staticCompositionLocalOf { Locale.getDefault().toLanguageTag() }
33 |
34 | val currentTag: String
35 | @Composable get() = LocalLocaleTag.current
36 |
37 | @Composable
38 | infix fun provides(localeTag: String?): ProvidedValue<*> {
39 | if (initialDefaultLocale == null) {
40 | initialDefaultLocale = Locale.getDefault()
41 | }
42 |
43 | val resolvedLocale =
44 | when (localeTag) {
45 | null -> initialDefaultLocale!!
46 | else -> Locale.forLanguageTag(localeTag)
47 | }
48 |
49 | Locale.setDefault(resolvedLocale)
50 |
51 | return LocalLocaleTag.provides(resolvedLocale.toLanguageTag())
52 | }
53 | }
54 |
55 | @Composable
56 | internal fun AppLocaleProvider(content: @Composable () -> Unit) {
57 | val locale = LocalPrefs.current.locale.toLanguageTag()
58 |
59 | CompositionLocalProvider(
60 | AppLocale provides locale,
61 | content = content,
62 | )
63 | }
64 |
65 | @Composable
66 | internal fun t(stringRes: StringResource) = stringResource(stringRes)
67 |
68 | @Composable
69 | internal fun t(
70 | stringRes: StringResource,
71 | vararg args: Any,
72 | ) = stringResource(stringRes, *args)
73 |
--------------------------------------------------------------------------------
/nbt/src/main/kotlin/com/spoiligaming/explorer/multiplayer/MultiplayerServer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | @file:OptIn(ExperimentalUuidApi::class)
20 |
21 | package com.spoiligaming.explorer.multiplayer
22 |
23 | import androidx.compose.runtime.Immutable
24 | import io.github.oshai.kotlinlogging.KotlinLogging
25 | import kotlin.io.encoding.Base64
26 | import kotlin.uuid.ExperimentalUuidApi
27 | import kotlin.uuid.Uuid
28 |
29 | @Immutable
30 | data class MultiplayerServer(
31 | val id: Uuid = Uuid.random(),
32 | val name: String,
33 | val ip: String,
34 | val iconBase64: String? = null,
35 | val hidden: HiddenState = HiddenState.NotHidden,
36 | val acceptTextures: AcceptTexturesState = AcceptTexturesState.Prompt,
37 | ) {
38 | val iconBytes
39 | get() =
40 | iconBase64
41 | ?.takeIf { it.isNotBlank() }
42 | ?.let {
43 | runCatching { Base64.decode(it) }
44 | .onFailure { e ->
45 | logger.error(e) { "Failed to decode icon for '$name': ${e.message}" }
46 | }.getOrNull()
47 | }?.takeIf { it.size <= MAX_ICON_SIZE_BYTES }
48 | ?: run {
49 | iconBase64?.let {
50 | logger.error {
51 | "Icon for '$name' too large (${it.length} bytes), " +
52 | "max is $MAX_ICON_SIZE_BYTES"
53 | }
54 | }
55 | null
56 | }
57 |
58 | val iconStream get() = iconBytes?.inputStream()
59 |
60 | companion object {
61 | const val MAX_ICON_SIZE_BYTES = 96 * 1024
62 | val logger = KotlinLogging.logger {}
63 | }
64 | }
65 |
66 | enum class AcceptTexturesState {
67 | Enabled,
68 | Disabled,
69 | Prompt,
70 | }
71 |
72 | enum class HiddenState {
73 | Hidden,
74 | NotHidden,
75 | }
76 |
--------------------------------------------------------------------------------
/core/src/test/kotlin/com/spoiligaming/explorer/minecraft/multiplayer/online/backend/mcsrvstat/McSrvStatServerStatusResponseTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.multiplayer.online.backend.mcsrvstat
20 |
21 | import kotlin.test.Test
22 | import kotlin.test.assertEquals
23 |
24 | internal class McSrvStatServerStatusResponseTest {
25 | private fun responseWithIcon(rawIcon: String) =
26 | McSrvStatServerStatusResponse(
27 | ip = "1.6.9",
28 | port = 25565,
29 | debug =
30 | DebugInfo(
31 | ping = false,
32 | query = false,
33 | bedrock = false,
34 | srv = false,
35 | querymismatch = false,
36 | ipinsrv = false,
37 | cnameinsrv = false,
38 | animatedmotd = false,
39 | cachehit = false,
40 | cachetime = 4,
41 | cacheexpire = 2,
42 | apiversion = 0,
43 | dns = DnsInfo(),
44 | error = null,
45 | ),
46 | motd = Motd(),
47 | players = Players(online = 0, max = 20),
48 | versionInfo = "4.20",
49 | online = true,
50 | protocol = Protocol(version = 764),
51 | hostname = "noballs727",
52 | rawIcon = rawIcon,
53 | info = null,
54 | eulaBlocked = false,
55 | )
56 |
57 | @Test
58 | fun `icon strips data URL base64 prefix`() {
59 | val raw = "data:image/png;base64,QUJDREVGRw=="
60 | val response = responseWithIcon(raw)
61 | assertEquals("QUJDREVGRw==", response.icon)
62 | }
63 |
64 | @Test
65 | fun `icon returns raw icon unchanged when no prefix`() {
66 | val raw = "QUJDREVGRw=="
67 | val response = responseWithIcon(raw)
68 | assertEquals(raw, response.icon)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/ui/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | plugins {
20 | alias(libs.plugins.compose)
21 | alias(libs.plugins.kotlin.compose)
22 | alias(libs.plugins.kotlin.serialization)
23 | }
24 |
25 | dependencies {
26 | implementation(project(":core"))
27 | implementation(project(":settings"))
28 | implementation(project(":nbt"))
29 |
30 | // Compose Desktop
31 | implementation(compose.desktop.currentOs) {
32 | exclude(group = "org.jetbrains.compose.material", module = "material")
33 | exclude(group = "org.jetbrains.compose.material", module = "material-desktop")
34 | exclude(group = "org.jetbrains.compose.material", module = "material-ripple")
35 | }
36 | implementation(compose.components.resources)
37 | implementation(compose.material3)
38 | implementation(compose.materialIconsExtended)
39 | implementation(libs.kotlinx.serialization.json)
40 |
41 | // Navigation Compose
42 | implementation(libs.navigation.compose)
43 |
44 | // UI Components
45 | implementation(libs.materialKolor)
46 | implementation(libs.color.picker)
47 | implementation(libs.file.kit)
48 | implementation(libs.composeShimmer)
49 | implementation(libs.reorderable.jvm)
50 | implementation(libs.autolinktext)
51 |
52 | // JNA for DWM
53 | implementation(libs.jna)
54 | implementation(libs.jna.platform)
55 |
56 | // System theme detection
57 | implementation(libs.theme.detector) {
58 | exclude(group = "net.java.dev.jna", module = "jna")
59 | exclude(group = "net.java.dev.jna", module = "jna-platform")
60 | }
61 |
62 | // Resources
63 | implementation(compose.components.resources)
64 | }
65 |
66 | compose {
67 | resources {
68 | generateResClass = always
69 |
70 | customDirectory(
71 | sourceSetName = "main",
72 | directoryProvider =
73 | provider {
74 | layout.projectDirectory.dir("src/main/composeResources")
75 | },
76 | )
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/multiplayer/online/backend/mcutils/retry.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.multiplayer.online.backend.mcutils
20 |
21 | internal class RetryPolicy(
22 | val exceptionClass: Class,
23 | val maxAttempts: Int,
24 | val onRetry: (attempt: Int, maxAttempts: Int, cause: Throwable) -> Unit,
25 | )
26 |
27 | internal inline fun retryPolicy(
28 | maxAttempts: Int,
29 | noinline onRetry: (attempt: Int, maxAttempts: Int, cause: E) -> Unit,
30 | ): RetryPolicy {
31 | require(maxAttempts >= 1) {
32 | "maxAttempts must be >= 1 (was $maxAttempts for ${E::class.simpleName})"
33 | }
34 |
35 | return RetryPolicy(
36 | exceptionClass = E::class.java,
37 | maxAttempts = maxAttempts,
38 | onRetry = { attempt, max, cause ->
39 | @Suppress("UNCHECKED_CAST")
40 | onRetry(attempt, max, cause as E)
41 | },
42 | )
43 | }
44 |
45 | internal inline fun retry(
46 | vararg policies: RetryPolicy,
47 | block: () -> T,
48 | ): T {
49 | if (policies.isEmpty()) return block()
50 |
51 | val size = policies.size
52 | val attemptsUsed = IntArray(size)
53 |
54 | while (true) {
55 | try {
56 | return block()
57 | } catch (t: Throwable) {
58 | var index = -1
59 | for (i in 0 until size) {
60 | if (policies[i].exceptionClass.isInstance(t)) {
61 | index = i
62 | break
63 | }
64 | }
65 |
66 | if (index == -1) {
67 | throw t
68 | }
69 |
70 | val policy = policies[index]
71 | val used = attemptsUsed[index]
72 |
73 | if (used + 1 >= policy.maxAttempts) {
74 | throw t
75 | }
76 |
77 | attemptsUsed[index] = used + 1
78 | policy.onRetry(used, policy.maxAttempts, t)
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/multiplayer/online/backend/mcsrvstat/McSrvStatServerQueryHandler.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.multiplayer.online.backend.mcsrvstat
20 |
21 | import com.spoiligaming.explorer.minecraft.multiplayer.online.backend.common.IServerQueryHandler
22 | import com.spoiligaming.explorer.minecraft.multiplayer.online.backend.common.McSrvStatRateLimitedServerData
23 | import com.spoiligaming.explorer.minecraft.multiplayer.online.backend.common.OfflineServerData
24 | import io.github.oshai.kotlinlogging.KotlinLogging
25 | import io.ktor.client.HttpClient
26 | import kotlinx.coroutines.Dispatchers
27 | import kotlinx.coroutines.withContext
28 |
29 | /**
30 | * `IServerQueryHandler` implementation powered by the public [https://api.mcsrvstat.us](https://api.mcsrvstat.us) service.
31 | */
32 | internal class McSrvStatServerQueryHandler(
33 | private val serverAddress: String,
34 | private val client: HttpClient,
35 | ) : IServerQueryHandler {
36 | override suspend fun getServerData() =
37 | withContext(Dispatchers.IO) {
38 | try {
39 | val rawJson =
40 | RequestHandler(serverAddress, client)
41 | .fetchResponseBody()
42 | .getOrThrow()
43 |
44 | if (!rawJson.contains("\"online\":true")) {
45 | logger.warn { "Server is offline: $serverAddress" }
46 | return@withContext OfflineServerData
47 | }
48 |
49 | val decoded = OnlineServerDataSerializer.decode(rawJson)
50 | OnlineServerDataSerializer.reduce(decoded)
51 | } catch (_: RequestHandler.RateLimitException) {
52 | McSrvStatRateLimitedServerData
53 | } catch (e: Exception) {
54 | logger.error(e) { "Failed to get server data for $serverAddress" }
55 | OfflineServerData
56 | }
57 | }
58 | }
59 |
60 | private val logger = KotlinLogging.logger {}
61 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/screens/setup/steps/LanguageSelectionStep.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | @file:OptIn(ExperimentalMaterial3Api::class)
20 |
21 | package com.spoiligaming.explorer.ui.screens.setup.steps
22 |
23 | import androidx.compose.foundation.layout.Arrangement
24 | import androidx.compose.foundation.layout.Column
25 | import androidx.compose.material3.ExperimentalMaterial3Api
26 | import androidx.compose.material3.MaterialTheme
27 | import androidx.compose.material3.Text
28 | import androidx.compose.runtime.Composable
29 | import androidx.compose.ui.unit.dp
30 | import com.spoiligaming.explorer.settings.manager.preferenceSettingsManager
31 | import com.spoiligaming.explorer.ui.com.spoiligaming.explorer.ui.LocalPrefs
32 | import com.spoiligaming.explorer.ui.screens.setup.SetupStepContainer
33 | import com.spoiligaming.explorer.ui.t
34 | import com.spoiligaming.explorer.ui.widgets.LanguagePickerDropdownMenu
35 | import server_list_explorer.ui.generated.resources.Res
36 | import server_list_explorer.ui.generated.resources.preferred_language_label
37 | import server_list_explorer.ui.generated.resources.setup_step_title_localization
38 |
39 | @Composable
40 | internal fun LanguageSelectionStep() {
41 | val currentLocale = LocalPrefs.current.locale
42 |
43 | SetupStepContainer(title = t(Res.string.setup_step_title_localization)) {
44 | Column(
45 | verticalArrangement = Arrangement.spacedBy(LanguageStepItemSpacing),
46 | ) {
47 | Text(
48 | text = t(Res.string.preferred_language_label),
49 | style = MaterialTheme.typography.titleMedium,
50 | color = MaterialTheme.colorScheme.onSurfaceVariant,
51 | )
52 | LanguagePickerDropdownMenu(
53 | selectedLocale = currentLocale,
54 | onLocaleSelected = { locale ->
55 | preferenceSettingsManager.updateSettings {
56 | it.copy(locale = locale)
57 | }
58 | },
59 | )
60 | }
61 | }
62 | }
63 |
64 | private val LanguageStepItemSpacing = 4.dp
65 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/snackbar/SnackbarController.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | @file:OptIn(ExperimentalAtomicApi::class)
20 |
21 | package com.spoiligaming.explorer.ui.snackbar
22 |
23 | import androidx.compose.material3.SnackbarDuration
24 | import com.spoiligaming.explorer.ui.snackbar.SnackbarController.FIRST_SNACKBAR_DELAY_MS
25 | import io.github.oshai.kotlinlogging.KotlinLogging
26 | import kotlinx.coroutines.channels.Channel
27 | import kotlinx.coroutines.delay
28 | import kotlinx.coroutines.flow.receiveAsFlow
29 | import kotlin.concurrent.atomics.AtomicBoolean
30 | import kotlin.concurrent.atomics.ExperimentalAtomicApi
31 |
32 | internal data class SnackbarEvent(
33 | val message: String,
34 | val duration: SnackbarDuration,
35 | val action: SnackbarAction? = null,
36 | )
37 |
38 | internal data class SnackbarAction(
39 | val name: String,
40 | val action: suspend () -> Unit,
41 | )
42 |
43 | internal object SnackbarController {
44 | private val logging = KotlinLogging.logger { }
45 |
46 | private const val FIRST_SNACKBAR_DELAY_MS = 600L
47 |
48 | private val _events = Channel()
49 | val events = _events.receiveAsFlow()
50 |
51 | private val firstSend = AtomicBoolean(true)
52 |
53 | /**
54 | * Sends a [SnackbarEvent] to the UI snackbar host.
55 | *
56 | * On the very first invocation after app startup, this function intentionally applies a short delay
57 | * (see [FIRST_SNACKBAR_DELAY_MS]) before sending the event. This gives the Compose UI on desktop
58 | * enough time to fully initialize, preventing a noticeable UI hitch or lag when showing the very first snackbar.
59 | *
60 | * All subsequent snackbar events are sent immediately without delay.
61 | */
62 | suspend fun sendEvent(event: SnackbarEvent) {
63 | if (firstSend.compareAndExchange(expectedValue = true, newValue = false)) {
64 | delay(FIRST_SNACKBAR_DELAY_MS)
65 | logging.debug {
66 | "Applied one-time delay of $FIRST_SNACKBAR_DELAY_MS ms before first snackbar event."
67 | }
68 | }
69 | _events.send(event)
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/navigation/NavGraph.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.navigation
20 |
21 | import androidx.compose.animation.EnterTransition
22 | import androidx.compose.animation.ExitTransition
23 | import androidx.compose.foundation.layout.fillMaxSize
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.ui.Modifier
26 | import androidx.navigation.NavHostController
27 | import androidx.navigation.compose.NavHost
28 | import androidx.navigation.compose.composable
29 | import com.spoiligaming.explorer.settings.manager.UniversalSettingsManager
30 | import com.spoiligaming.explorer.ui.AppLocaleProvider
31 | import com.spoiligaming.explorer.ui.components.LoadingScreen
32 | import com.spoiligaming.explorer.ui.screens.multiplayer.MultiplayerScreenContainer
33 | import com.spoiligaming.explorer.ui.screens.settings.SettingsScreen
34 | import com.spoiligaming.explorer.ui.t
35 | import server_list_explorer.ui.generated.resources.Res
36 | import server_list_explorer.ui.generated.resources.loading_user_settings
37 |
38 | @Composable
39 | internal fun NavGraph(navController: NavHostController) {
40 | NavHost(
41 | navController = navController,
42 | startDestination = MultiplayerServerListScreen,
43 | enterTransition = { EnterTransition.None },
44 | exitTransition = { ExitTransition.None },
45 | popEnterTransition = { EnterTransition.None },
46 | popExitTransition = { ExitTransition.None },
47 | ) {
48 | composable {
49 | MultiplayerScreenContainer(navController)
50 | }
51 |
52 | composable {
53 | AppLocaleProvider {
54 | LoadingScreen(
55 | displayAfterThreshold = true,
56 | steps =
57 | listOf(
58 | t(Res.string.loading_user_settings) to { UniversalSettingsManager.loadAll() },
59 | ),
60 | modifier = Modifier.fillMaxSize(),
61 | ) {
62 | SettingsScreen()
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/multiplayer/online/backend/mcsrvstat/RequestHandler.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.multiplayer.online.backend.mcsrvstat
20 |
21 | import io.github.oshai.kotlinlogging.KotlinLogging
22 | import io.ktor.client.HttpClient
23 | import io.ktor.client.request.get
24 | import io.ktor.client.statement.bodyAsText
25 | import io.ktor.http.HttpStatusCode
26 | import io.ktor.http.headers
27 |
28 | private val logger = KotlinLogging.logger {}
29 |
30 | internal class RequestHandler(
31 | serverAddress: String,
32 | private val client: HttpClient,
33 | ) {
34 | companion object {
35 | private const val BASE_URL = "https://api.mcsrvstat.us/3"
36 | }
37 |
38 | init {
39 | require(serverAddress.isNotBlank()) { "Server address must not be blank" }
40 | }
41 |
42 | private val serverAddress = serverAddress.trim()
43 |
44 | internal class RateLimitException : IllegalStateException("Rate limit exceeded for MCSrvStatus")
45 |
46 | suspend fun fetchResponseBody(): Result {
47 | val url = "$BASE_URL/$serverAddress"
48 | logger.debug { "Requesting server status at $url" }
49 |
50 | return runCatching {
51 | val response =
52 | client.get(url) {
53 | headers {
54 | append(
55 | "User-Agent",
56 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
57 | )
58 | append("Accept", "application/json")
59 | }
60 | }
61 |
62 | when (response.status) {
63 | HttpStatusCode.TooManyRequests -> throw RateLimitException()
64 | HttpStatusCode.OK -> {
65 | val body = response.bodyAsText()
66 | check(body.isNotBlank()) { "Response body is empty" }
67 | body
68 | }
69 |
70 | else -> error("Unexpected HTTP status: ${response.status.value}")
71 | }
72 | }.onFailure { e ->
73 | logger.error(e) { "Failed to fetch status for $serverAddress" }
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/extensions/ImageBitmapLuminance.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.extensions
20 |
21 | import androidx.compose.ui.geometry.Rect
22 | import androidx.compose.ui.graphics.ImageBitmap
23 | import androidx.compose.ui.graphics.toPixelMap
24 |
25 | // luminance coefficients per ITU-R BT.709
26 | private const val LUMINANCE_RED = 0.2126f
27 | private const val LUMINANCE_GREEN = 0.7152f
28 | private const val LUMINANCE_BLUE = 0.0722f
29 |
30 | private const val DEFAULT_THRESHOLD = 0.5f
31 | private const val DEFAULT_STRIDE = 4
32 |
33 | /**
34 | * Computes the average luminance of a region in the bitmap.
35 | *
36 | * @param region The rectangular area to sample (in pixels). Defaults to the entire bitmap.
37 | * @param stride Number of pixels to skip per sample (higher = faster, lower = more accurate).
38 | * @return Average luminance in [0,1], or 1f if no samples were taken.
39 | */
40 | private fun ImageBitmap.averageLuminance(
41 | region: Rect = Rect(0f, 0f, width.toFloat(), height.toFloat()),
42 | stride: Int = DEFAULT_STRIDE,
43 | ): Float {
44 | val pixelMap = toPixelMap()
45 | // clamp region to bitmap bounds
46 | val left = region.left.coerceIn(0f, width.toFloat()).toInt()
47 | val top = region.top.coerceIn(0f, height.toFloat()).toInt()
48 | val right = region.right.coerceIn(0f, width.toFloat()).toInt()
49 | val bottom = region.bottom.coerceIn(0f, height.toFloat()).toInt()
50 |
51 | var sum = 0f
52 | var count = 0
53 |
54 | for (y in top until bottom step stride) {
55 | for (x in left until right step stride) {
56 | pixelMap[x, y].let { color ->
57 | sum += color.red * LUMINANCE_RED +
58 | color.green * LUMINANCE_GREEN +
59 | color.blue * LUMINANCE_BLUE
60 | }
61 | count++
62 | }
63 | }
64 | return if (count > 0) sum / count else 1f
65 | }
66 |
67 | /**
68 | * Determines if the entire bitmap is considered dark.
69 | *
70 | * @param threshold Luminance cutoff (0 = pure black, 1 = pure white).
71 | * @param stride Sampling stride.
72 | */
73 | internal fun ImageBitmap.isDark(
74 | threshold: Float = DEFAULT_THRESHOLD,
75 | stride: Int = DEFAULT_STRIDE,
76 | ) = averageLuminance(stride = stride) < threshold
77 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/components/FPSCounterOverlay.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.components
20 |
21 | import androidx.compose.foundation.layout.Box
22 | import androidx.compose.foundation.layout.BoxScope
23 | import androidx.compose.foundation.layout.padding
24 | import androidx.compose.material3.CardDefaults
25 | import androidx.compose.material3.ElevatedCard
26 | import androidx.compose.material3.MaterialTheme
27 | import androidx.compose.material3.Text
28 | import androidx.compose.runtime.Composable
29 | import androidx.compose.runtime.LaunchedEffect
30 | import androidx.compose.runtime.getValue
31 | import androidx.compose.runtime.mutableIntStateOf
32 | import androidx.compose.runtime.remember
33 | import androidx.compose.runtime.setValue
34 | import androidx.compose.runtime.withFrameNanos
35 | import androidx.compose.ui.Alignment
36 | import androidx.compose.ui.Modifier
37 | import androidx.compose.ui.unit.dp
38 | import androidx.compose.ui.unit.sp
39 | import org.jetbrains.skiko.FPSCounter
40 |
41 | @Composable
42 | internal fun FPSOverlay(onFpsUpdate: (Int) -> Unit) =
43 | LaunchedEffect(Unit) {
44 | val counter = FPSCounter(logOnTick = false)
45 | while (true) {
46 | withFrameNanos {
47 | counter.tick()
48 | val fps = counter.average
49 | onFpsUpdate(fps)
50 | }
51 | }
52 | }
53 |
54 | @Composable
55 | internal fun BoxScope.FPSDisplay() {
56 | var fps by remember { mutableIntStateOf(0) }
57 | FPSOverlay { fps = it }
58 | Box(
59 | Modifier.align(Alignment.BottomEnd),
60 | ) {
61 | ElevatedCard(
62 | shape = MaterialTheme.shapes.extraSmall,
63 | colors =
64 | CardDefaults.cardColors(
65 | containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
66 | ),
67 | ) {
68 | Text(
69 | text = "$fps FPS",
70 | modifier = Modifier.padding(FpsCardPadding),
71 | style =
72 | MaterialTheme.typography.bodyLarge.copy(
73 | fontSize = FpsTextSize,
74 | ),
75 | )
76 | }
77 | }
78 | }
79 |
80 | private val FpsCardPadding = 4.dp
81 | private val FpsTextSize = 16.sp
82 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 |
74 |
75 | @rem Execute Gradle
76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
77 |
78 | :end
79 | @rem End local scope for the variables with windows NT shell
80 | if %ERRORLEVEL% equ 0 goto mainEnd
81 |
82 | :fail
83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
84 | rem the _cmd.exe /c_ return code!
85 | set EXIT_CODE=%ERRORLEVEL%
86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
88 | exit /b %EXIT_CODE%
89 |
90 | :mainEnd
91 | if "%OS%"=="Windows_NT" endlocal
92 |
93 | :omega
94 |
--------------------------------------------------------------------------------
/settings/src/main/kotlin/com/spoiligaming/explorer/settings/manager/SettingsManagers.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.settings.manager
20 |
21 | import com.spoiligaming.explorer.settings.model.MultiplayerSettings
22 | import com.spoiligaming.explorer.settings.model.Preferences
23 | import com.spoiligaming.explorer.settings.model.ServerQueryMethodConfigurations
24 | import com.spoiligaming.explorer.settings.model.SingleplayerSettings
25 | import com.spoiligaming.explorer.settings.model.ThemeSettings
26 | import com.spoiligaming.explorer.settings.model.WindowAppearance
27 | import com.spoiligaming.explorer.settings.model.WindowState
28 |
29 | /*
30 | * CONTRIBUTOR NOTICE:
31 | *
32 | * If you add, remove, or rename a UniversalSettingsManager or any settings model in this file,
33 | * you must also update com.spoiligaming.explorer.ui.SettingsCompositionLocals.kt
34 | * to keep the CompositionLocals in sync with these managers.
35 | *
36 | * Failing to do so will break global settings propagation in the UI.
37 | */
38 |
39 | val windowStateSettingsManager by UniversalSettingsManager(
40 | fileName = "window_state.json",
41 | defaultValueProvider = { WindowState() },
42 | )
43 |
44 | val windowAppearanceSettingsManager by UniversalSettingsManager(
45 | fileName = "window_appearance.json",
46 | defaultValueProvider = { WindowAppearance() },
47 | )
48 |
49 | val preferenceSettingsManager by UniversalSettingsManager(
50 | fileName = "preferences.json",
51 | defaultValueProvider = { Preferences() },
52 | )
53 |
54 | val themeSettingsManager by UniversalSettingsManager(
55 | fileName = "theme.json",
56 | defaultValueProvider = { ThemeSettings() },
57 | )
58 |
59 | val multiplayerSettingsManager by UniversalSettingsManager(
60 | fileName = "multiplayer.json",
61 | defaultValueProvider = { MultiplayerSettings() },
62 | )
63 |
64 | val serverQueryMethodConfigurationsManager by UniversalSettingsManager(
65 | fileName = "server_query_method_configs.json",
66 | defaultValueProvider = { ServerQueryMethodConfigurations() },
67 | )
68 |
69 | val singleplayerSettingsManager by UniversalSettingsManager(
70 | fileName = "singleplayer.json",
71 | defaultValueProvider = { SingleplayerSettings() },
72 | )
73 |
--------------------------------------------------------------------------------
/add_gpl_header.bat:
--------------------------------------------------------------------------------
1 | rem This script was generated with assistance from ChatGPT-4o by OpenAI.
2 |
3 | @echo off
4 | setlocal EnableDelayedExpansion
5 |
6 | set "header_kt_0=/*"
7 | set "header_kt_1= * This file is part of Server List Explorer."
8 | set "header_kt_2= * Copyright (C) 2025 SpoilerRules"
9 | set "header_kt_3= *"
10 | set "header_kt_4= * Server List Explorer is free software: you can redistribute it and/or modify"
11 | set "header_kt_5= * it under the terms of the GNU General Public License as published by"
12 | set "header_kt_6= * the Free Software Foundation, either version 3 of the License, or"
13 | set "header_kt_7= * (at your option) any later version."
14 | set "header_kt_8= *"
15 | set "header_kt_9= * Server List Explorer is distributed in the hope that it will be useful,"
16 | set "header_kt_10= * but WITHOUT ANY WARRANTY; without even the implied warranty of"
17 | set "header_kt_11= * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the"
18 | set "header_kt_12= * GNU General Public License for more details."
19 | set "header_kt_13= *"
20 | set "header_kt_14= * You should have received a copy of the GNU General Public License"
21 | set "header_kt_15= * along with Server List Explorer. If not, see ."
22 | set "header_kt_16=*/"
23 |
24 | set "header_prop_0=# This file is part of Server List Explorer."
25 | set "header_prop_1=# Copyright (C) 2025 SpoilerRules"
26 | set "header_prop_2=#"
27 | set "header_prop_3=# Server List Explorer is free software: you can redistribute it and/or modify"
28 | set "header_prop_4=# it under the terms of the GNU General Public License as published by"
29 | set "header_prop_5=# the Free Software Foundation, either version 3 of the License, or"
30 | set "header_prop_6=# (at your option) any later version."
31 | set "header_prop_7=#"
32 | set "header_prop_8=# Server List Explorer is distributed in the hope that it will be useful,"
33 | set "header_prop_9=# but WITHOUT ANY WARRANTY; without even the implied warranty of"
34 | set "header_prop_10=# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the"
35 | set "header_prop_11=# GNU General Public License for more details."
36 | set "header_prop_12=#"
37 | set "header_prop_13=# You should have received a copy of the GNU General Public License"
38 | set "header_prop_14=# along with Server List Explorer. If not, see ."
39 |
40 | for /R %%F in (*.kt *.kts) do (
41 | findstr /C:"* This file is part of Server List Explorer." "%%F" >nul
42 | if errorlevel 1 (
43 | echo Adding header to %%F
44 | >"%%F.temp" (
45 | for /L %%i in (0,1,16) do echo !header_kt_%%i!
46 | echo(
47 | type "%%F"
48 | )
49 | move /Y "%%F.temp" "%%F" >nul
50 | )
51 | )
52 |
53 | for /R %%F in (*.properties) do (
54 | findstr /C:"# This file is part of Server List Explorer." "%%F" >nul
55 | if errorlevel 1 (
56 | echo Adding header to %%F
57 | >"%%F.temp" (
58 | for /L %%i in (0,1,14) do echo !header_prop_%%i!
59 | echo(
60 | type "%%F"
61 | )
62 | move /Y "%%F.temp" "%%F" >nul
63 | )
64 | )
65 |
66 | echo.
67 | echo Done.
68 | pause
69 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/screens/setup/SetupStepContainer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.screens.setup
20 |
21 | import androidx.compose.foundation.layout.Arrangement
22 | import androidx.compose.foundation.layout.Box
23 | import androidx.compose.foundation.layout.BoxScope
24 | import androidx.compose.foundation.layout.Column
25 | import androidx.compose.foundation.layout.fillMaxSize
26 | import androidx.compose.foundation.layout.padding
27 | import androidx.compose.foundation.layout.widthIn
28 | import androidx.compose.foundation.layout.wrapContentHeight
29 | import androidx.compose.material3.Card
30 | import androidx.compose.material3.MaterialTheme
31 | import androidx.compose.material3.Text
32 | import androidx.compose.runtime.Composable
33 | import androidx.compose.ui.Alignment
34 | import androidx.compose.ui.Modifier
35 | import androidx.compose.ui.unit.dp
36 |
37 | @Composable
38 | internal fun SetupStepContainer(
39 | title: String,
40 | subtitle: String? = null,
41 | content: @Composable BoxScope.() -> Unit,
42 | ) = Box(
43 | modifier = Modifier.fillMaxSize(),
44 | contentAlignment = Alignment.Center,
45 | ) {
46 | Card(
47 | modifier =
48 | Modifier
49 | .widthIn(min = ContainerMinWidth, max = ContainerMaxWidth)
50 | .wrapContentHeight(),
51 | ) {
52 | Column(
53 | modifier = Modifier.padding(ContainerPadding),
54 | verticalArrangement = Arrangement.spacedBy(ColumnArrangement),
55 | ) {
56 | Column {
57 | Text(
58 | text = title,
59 | style = MaterialTheme.typography.titleLarge,
60 | color = MaterialTheme.colorScheme.onSurface,
61 | )
62 | subtitle?.let {
63 | Text(
64 | text = it,
65 | style = MaterialTheme.typography.bodyMedium,
66 | color = MaterialTheme.colorScheme.onSurfaceVariant,
67 | )
68 | }
69 | }
70 | Box {
71 | content()
72 | }
73 | }
74 | }
75 | }
76 |
77 | private val ContainerPadding = 32.dp
78 | private val ColumnArrangement = 16.dp
79 | private val ContainerMinWidth = 400.dp
80 | private val ContainerMaxWidth = 800.dp
81 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/window/WindowState.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.window
20 |
21 | import com.spoiligaming.explorer.settings.manager.windowStateSettingsManager
22 | import java.awt.Frame
23 | import java.awt.event.ComponentAdapter
24 | import java.awt.event.ComponentEvent
25 | import java.awt.event.WindowEvent
26 | import javax.swing.JFrame
27 |
28 | internal object WindowSettingsBinder {
29 | @Suppress("NOTHING_TO_INLINE")
30 | inline fun JFrame.listenResizes() {
31 | addComponentListener(
32 | object : ComponentAdapter() {
33 | override fun componentResized(event: ComponentEvent) {
34 | val frame = event.component as JFrame
35 | // same surgical procedure as in listenMaximization
36 | val isMaximized = frame.extendedState and Frame.MAXIMIZED_BOTH == Frame.MAXIMIZED_BOTH
37 |
38 | frame.size.let { newSize ->
39 | windowStateSettingsManager.updateSettings { current ->
40 | if (isMaximized) {
41 | current.copy(
42 | currentWidth = newSize.width,
43 | currentHeight = newSize.height,
44 | )
45 | } else {
46 | current.copy(
47 | width = newSize.width,
48 | height = newSize.height,
49 | currentWidth = newSize.width,
50 | currentHeight = newSize.height,
51 | )
52 | }
53 | }
54 | }
55 | }
56 | },
57 | )
58 | }
59 |
60 | @Suppress("NOTHING_TO_INLINE")
61 | inline fun JFrame.listenMaximization() {
62 | addWindowStateListener { event: WindowEvent ->
63 | // check if both bits of MAXIMIZED_BOTH are set
64 | val isMaximized = event.newState and Frame.MAXIMIZED_BOTH == Frame.MAXIMIZED_BOTH
65 |
66 | windowStateSettingsManager.updateSettings {
67 | it.copy(
68 | isWindowMaximized = isMaximized,
69 | )
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/screens/multiplayer/querymethod/items/QueryMethodCheckboxItem.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.screens.multiplayer.querymethod.items
20 |
21 | import androidx.compose.foundation.layout.Arrangement
22 | import androidx.compose.foundation.layout.Column
23 | import androidx.compose.foundation.layout.Row
24 | import androidx.compose.foundation.layout.fillMaxWidth
25 | import androidx.compose.material3.Checkbox
26 | import androidx.compose.material3.MaterialTheme
27 | import androidx.compose.material3.Text
28 | import androidx.compose.runtime.Composable
29 | import androidx.compose.ui.Alignment
30 | import androidx.compose.ui.Modifier
31 | import androidx.compose.ui.input.pointer.PointerIcon
32 | import androidx.compose.ui.input.pointer.pointerHoverIcon
33 | import androidx.compose.ui.unit.dp
34 | import com.spoiligaming.explorer.ui.screens.multiplayer.querymethod.QueryMethodCheckboxSpec
35 | import com.spoiligaming.explorer.ui.widgets.PocketInfoTooltip
36 |
37 | @Composable
38 | internal fun QueryMethodCheckboxItem(
39 | spec: QueryMethodCheckboxSpec,
40 | modifier: Modifier = Modifier,
41 | ) = Column(
42 | modifier = modifier,
43 | ) {
44 | Row(
45 | modifier = Modifier.fillMaxWidth(),
46 | verticalAlignment = Alignment.CenterVertically,
47 | horizontalArrangement = Arrangement.SpaceBetween,
48 | ) {
49 | Row(
50 | modifier = Modifier.weight(TITLE_WEIGHT),
51 | verticalAlignment = Alignment.CenterVertically,
52 | horizontalArrangement = Arrangement.spacedBy(TitleSpacing),
53 | ) {
54 | Text(
55 | text = spec.title,
56 | style = MaterialTheme.typography.labelLarge,
57 | color = MaterialTheme.colorScheme.onSurface,
58 | )
59 | if (!spec.description.isNullOrBlank()) {
60 | PocketInfoTooltip(
61 | text = spec.description,
62 | minWidth = TooltipMinWidth,
63 | )
64 | }
65 | }
66 | Checkbox(
67 | checked = spec.checked,
68 | onCheckedChange = spec.onCheckedChange,
69 | modifier = Modifier.pointerHoverIcon(PointerIcon.Hand),
70 | )
71 | }
72 | }
73 |
74 | private val TitleSpacing = 4.dp
75 | private val TooltipMinWidth = 296.dp
76 | private const val TITLE_WEIGHT = 1f
77 |
--------------------------------------------------------------------------------
/settings/src/main/kotlin/com/spoiligaming/explorer/settings/model/Preferences.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.settings.model
20 |
21 | import com.spoiligaming.explorer.settings.serializer.LocaleSerializer
22 | import kotlinx.serialization.SerialName
23 | import kotlinx.serialization.Serializable
24 | import java.util.Locale
25 |
26 | @Serializable
27 | data class Preferences(
28 | @SerialName("locale")
29 | @Serializable(with = LocaleSerializer::class)
30 | val locale: Locale = determineDefaultLocale(),
31 | @SerialName("snackbar_at_top")
32 | val snackbarAtTop: Boolean = false,
33 | @SerialName("max_undo_history_size")
34 | val maxUndoHistorySize: Int = 25,
35 | @SerialName("undo_redo_repeat_initial_delay_ms")
36 | val undoRedoRepeatInitialDelayMillis: Long = 400,
37 | @SerialName("undo_redo_repeat_interval_ms")
38 | val undoRedoRepeatIntervalMillis: Long = 50,
39 | @SerialName("scroll_after_add")
40 | val scrollAfterAdd: Boolean = true,
41 | @SerialName("highlight_after_scroll")
42 | val highlightAfterScroll: Boolean = true,
43 | @SerialName("highlight_after_scroll_delay_ms")
44 | val highlightAfterScrollDelayMillis: Long = 300,
45 | @SerialName("vsync")
46 | val vsync: Boolean = true,
47 | @SerialName("show_fps_overlay")
48 | val showFpsOverlay: Boolean = false,
49 | @SerialName("window_title_show_build_info")
50 | val windowTitleShowBuildInfo: Boolean = false,
51 | @SerialName("scrollbar_always_visible")
52 | val settingsScrollbarAlwaysVisible: Boolean = false,
53 | @SerialName("nav_rail_items_centered")
54 | val navRailItemsCentered: Boolean = true,
55 | ) {
56 | companion object {
57 | /**
58 | * Returns the app's default locale, applying overrides for English regional variants
59 | * that typically follow British spelling.
60 | */
61 | fun determineDefaultLocale(): Locale {
62 | val systemLocale = Locale.getDefault()
63 |
64 | return when (systemLocale.language to systemLocale.country) {
65 | "en" to "AU",
66 | "en" to "NZ",
67 | "en" to "CA",
68 | "en" to "IE",
69 | "en" to "ZA",
70 | "en" to "HK",
71 | "en" to "IN",
72 | -> Locale.UK
73 |
74 | else -> systemLocale
75 | }
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/screens/multiplayer/ServerSelectionController.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | @file:OptIn(ExperimentalUuidApi::class)
20 |
21 | package com.spoiligaming.explorer.ui.screens.multiplayer
22 |
23 | import kotlinx.coroutines.flow.MutableStateFlow
24 | import kotlinx.coroutines.flow.asStateFlow
25 | import kotlin.uuid.ExperimentalUuidApi
26 | import kotlin.uuid.Uuid
27 |
28 | internal class ServerSelectionController(
29 | private val onSelectionChanged: (() -> Unit)?,
30 | ) {
31 | private val _selectedIds = MutableStateFlow>(emptySet())
32 | val selectedIds = _selectedIds.asStateFlow()
33 |
34 | private var anchorKey: Uuid? = null
35 |
36 | fun toggle(id: Uuid) {
37 | _selectedIds.value = _selectedIds.value.toggle(id)
38 | anchorKey = id
39 | onSelectionChanged?.invoke()
40 | }
41 |
42 | fun clear() {
43 | _selectedIds.value = emptySet()
44 | anchorKey = null
45 | onSelectionChanged?.invoke()
46 | }
47 |
48 | fun selectAll(allIds: Collection) {
49 | _selectedIds.value = allIds.toSet()
50 | anchorKey = allIds.firstOrNull()
51 | onSelectionChanged?.invoke()
52 | }
53 |
54 | fun handlePointerClick(
55 | id: Uuid,
56 | entries: List,
57 | ctrlMeta: Boolean,
58 | shift: Boolean,
59 | ) {
60 | val idx = entries.indexOf(id)
61 | val anchorIdx = anchorKey?.let { entries.indexOf(it) }?.takeIf { it >= 0 }
62 |
63 | when {
64 | // multi-select
65 | ctrlMeta -> toggle(id)
66 |
67 | // range-select
68 | shift && anchorIdx != null -> {
69 | val range = if (idx >= anchorIdx) anchorIdx..idx else idx..anchorIdx
70 | _selectedIds.value = range.map { entries[it] }.toSet()
71 | onSelectionChanged?.invoke()
72 | }
73 |
74 | // single-select
75 | else -> {
76 | _selectedIds.value = setOf(id)
77 | anchorKey = id
78 | onSelectionChanged?.invoke()
79 | }
80 | }
81 | }
82 |
83 | fun indicesOf(entries: List) =
84 | entries.mapIndexedNotNull { index, uuid ->
85 | if (uuid in _selectedIds.value) index else null
86 | }
87 |
88 | private fun Set.toggle(key: Uuid) = if (key in this) this - key else this + key
89 | }
90 |
--------------------------------------------------------------------------------
/settings/src/main/kotlin/com/spoiligaming/explorer/settings/model/WindowAppearence.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.settings.model
20 |
21 | import com.spoiligaming.explorer.settings.model.WindowCornerPreferenceSetting.ELEVATED_SQUARE
22 | import com.spoiligaming.explorer.settings.model.WindowCornerPreferenceSetting.FLAT_SQUARE
23 | import com.spoiligaming.explorer.settings.model.WindowCornerPreferenceSetting.ROUNDED
24 | import com.spoiligaming.explorer.settings.model.WindowCornerPreferenceSetting.SYSTEM_DEFAULT
25 | import kotlinx.serialization.SerialName
26 | import kotlinx.serialization.Serializable
27 |
28 | /**
29 | * Represents the window corner styling preference, mapped to the underlying Windows DWM setting.
30 | *
31 | * These values correspond to how corners are rendered for top-level windows:
32 | * - [SYSTEM_DEFAULT] – Uses the system's default corner style (typically rounded on Windows 11).
33 | * - [ROUNDED] – Forces rounded corners.
34 | * - [ELEVATED_SQUARE] – Forces square corners with visual elevation (shadow).
35 | * - [FLAT_SQUARE] – Forces square corners without elevation, producing a flat appearance.
36 | *
37 | * Note: Rounded corners require Windows 11 (build 22000 or later).
38 | *
39 | * @see
40 | * DWM_WINDOW_CORNER_PREFERENCE – Windows API Documentation
41 | *
42 | */
43 | @Serializable
44 | enum class WindowCornerPreferenceSetting(
45 | val dwmValue: Int,
46 | ) {
47 | @SerialName("system_default")
48 | SYSTEM_DEFAULT(0), // DWMWCP_DEFAULT
49 |
50 | @SerialName("rounded")
51 | ROUNDED(2), // DWMWCP_ROUND,
52 |
53 | @SerialName("square_elevated")
54 | ELEVATED_SQUARE(1), // DWMWCP_DONOTROUND
55 |
56 | @SerialName("square_flat")
57 | FLAT_SQUARE(3), // DWMWCP_ROUNDSMALL
58 | }
59 |
60 | @Serializable
61 | enum class TitleBarColorMode {
62 | @SerialName("auto")
63 | AUTO,
64 |
65 | @SerialName("manual")
66 | MANUAL,
67 | }
68 |
69 | @Serializable
70 | data class WindowAppearance(
71 | @SerialName("corner_preference")
72 | val windowCornerPreference: WindowCornerPreferenceSetting = SYSTEM_DEFAULT,
73 | @SerialName("titlebar_color_mode")
74 | val titleBarColorMode: TitleBarColorMode = TitleBarColorMode.AUTO,
75 | @SerialName("custom_title_bar_color")
76 | val customTitleBarColor: String = "#FFFFFF",
77 | @SerialName("use_custom_border_color")
78 | val useCustomBorderColor: Boolean = false,
79 | @SerialName("custom_border_color")
80 | val customBorderColor: String = "#FFFFFF",
81 | )
82 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/util/AdaptiveDimension.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.util
20 |
21 | import androidx.compose.runtime.Composable
22 | import androidx.compose.runtime.derivedStateOf
23 | import androidx.compose.runtime.getValue
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.ui.platform.LocalDensity
26 | import androidx.compose.ui.unit.Dp
27 | import androidx.compose.ui.unit.dp
28 | import com.spoiligaming.explorer.ui.com.spoiligaming.explorer.ui.LocalWindowState
29 |
30 | /**
31 | * Computes an adaptive width based on the current window dimensions, clamped between [min] and [max].
32 | *
33 | * @param min The minimum width (default: 0.dp)
34 | * @param max The maximum width (default: Dp.Infinity)
35 | * @param fraction The fraction of the window width to use (default: 0.25f)
36 | * @return The clamped adaptive width in Dp.
37 | */
38 | @Composable
39 | internal fun rememberAdaptiveWidth(
40 | min: Dp = DefaultMinDp,
41 | max: Dp = Dp.Infinity,
42 | fraction: Float = DEFAULT_ADAPTIVE_FRACTION,
43 | ): Dp {
44 | val windowState = LocalWindowState.current
45 | val density = LocalDensity.current
46 |
47 | val adaptive by remember(windowState, min, max, fraction) {
48 | derivedStateOf {
49 | val baseDp = with(density) { windowState.currentWidth.toDp() }
50 | val calculated = baseDp * fraction
51 |
52 | when {
53 | calculated < min -> min
54 | calculated > max -> max
55 | else -> calculated
56 | }
57 | }
58 | }
59 |
60 | return adaptive
61 | }
62 |
63 | /**
64 | * Computes an adaptive height based on the current window dimensions, clamped between [min] and [max].
65 | *
66 | * @param min The minimum height (default: 0.dp)
67 | * @param max The maximum height (default: Dp.Infinity)
68 | * @param fraction The fraction of the window height to use (default: 0.25f)
69 | * @return The clamped adaptive height in Dp.
70 | */
71 | @Composable
72 | internal fun rememberAdaptiveHeight(
73 | min: Dp = DefaultMinDp,
74 | max: Dp = Dp.Infinity,
75 | fraction: Float = DEFAULT_ADAPTIVE_FRACTION,
76 | ): Dp {
77 | val windowState = LocalWindowState.current
78 | val density = LocalDensity.current
79 |
80 | val adaptive by remember(windowState, min, max, fraction) {
81 | derivedStateOf {
82 | val baseDp = with(density) { windowState.currentHeight.toDp() }
83 | val calculated = baseDp * fraction
84 |
85 | calculated.coerceIn(min, max)
86 | }
87 | }
88 |
89 | return adaptive
90 | }
91 |
92 | private const val DEFAULT_ADAPTIVE_FRACTION = 0.25f
93 | private val DefaultMinDp = 0.dp
94 |
--------------------------------------------------------------------------------
/settings/src/main/kotlin/com/spoiligaming/explorer/settings/model/ServerQueryMethodConfigurations.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.settings.model
20 |
21 | import kotlinx.serialization.SerialName
22 | import kotlinx.serialization.Serializable
23 |
24 | @Serializable
25 | data class ServerQueryMethodConfigurations(
26 | @SerialName("mc_srv_status")
27 | val mcSrvStat: McSrvStatusQueryConfiguration = McSrvStatusQueryConfiguration(),
28 | @SerialName("mc_utils")
29 | val mcUtils: McUtilsQueryConfiguration = McUtilsQueryConfiguration(),
30 | )
31 |
32 | sealed interface QueryMethodRequestKey
33 |
34 | data class McSrvStatRequestKey(
35 | val connectTimeoutMillis: Long,
36 | val socketTimeoutMillis: Long,
37 | ) : QueryMethodRequestKey
38 |
39 | data class McUtilsRequestKey(
40 | val timeoutMillis: Long,
41 | val enableSrvLookups: Boolean,
42 | ) : QueryMethodRequestKey
43 |
44 | interface ServerQueryMethodConfig {
45 | val requestKey: QueryMethodRequestKey
46 | }
47 |
48 | @Serializable
49 | data class McSrvStatusQueryConfiguration(
50 | @SerialName("timeouts")
51 | val timeouts: McSrvStatusTimeouts = McSrvStatusTimeouts(),
52 | ) : ServerQueryMethodConfig {
53 | override val requestKey
54 | get() =
55 | McSrvStatRequestKey(
56 | connectTimeoutMillis = timeouts.connectionTimeoutMillis,
57 | socketTimeoutMillis = timeouts.responseTimeoutMillis,
58 | )
59 | }
60 |
61 | @Serializable
62 | data class McUtilsQueryConfiguration(
63 | @SerialName("timeouts")
64 | val timeouts: McUtilsTimeouts = McUtilsTimeouts(),
65 | @SerialName("options")
66 | val options: McUtilsQueryOptions = McUtilsQueryOptions(),
67 | ) : ServerQueryMethodConfig {
68 | override val requestKey
69 | get() =
70 | McUtilsRequestKey(
71 | timeoutMillis = timeouts.timeoutMillis,
72 | enableSrvLookups = options.enableSrvLookups,
73 | )
74 | }
75 |
76 | @Serializable
77 | data class McSrvStatusTimeouts(
78 | @SerialName("connection_timeout_ms")
79 | val connectionTimeoutMillis: Long = 120_000L,
80 | @SerialName("response_timeout_ms")
81 | val responseTimeoutMillis: Long = 15_000L,
82 | )
83 |
84 | @Serializable
85 | data class McUtilsTimeouts(
86 | @SerialName("timeout_ms")
87 | val timeoutMillis: Long = 45_000L,
88 | )
89 |
90 | @Serializable
91 | data class McUtilsQueryOptions(
92 | @SerialName("enable_srv")
93 | val enableSrvLookups: Boolean = true,
94 | )
95 |
96 | fun ServerQueryMethodConfigurations.requestKeyFor(method: ServerQueryMethod): QueryMethodRequestKey =
97 | when (method) {
98 | ServerQueryMethod.McSrvStat -> mcSrvStat.requestKey
99 | ServerQueryMethod.McUtils -> mcUtils.requestKey
100 | }
101 |
--------------------------------------------------------------------------------
/nbt/src/main/kotlin/com/spoiligaming/explorer/multiplayer/history/ServerListHistoryService.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.multiplayer.history
20 |
21 | import io.github.oshai.kotlinlogging.KotlinLogging
22 | import kotlinx.coroutines.flow.MutableStateFlow
23 | import kotlinx.coroutines.flow.asStateFlow
24 | import kotlinx.coroutines.sync.Mutex
25 | import kotlinx.coroutines.sync.withLock
26 |
27 | class ServerListHistoryService(
28 | private val maxUndoEntries: Int,
29 | ) {
30 | private val mutex = Mutex()
31 | private val undoStack = ArrayDeque()
32 | private val redoStack = ArrayDeque()
33 |
34 | private val _canUndo = MutableStateFlow(false)
35 | val canUndo = _canUndo.asStateFlow()
36 |
37 | private val _canRedo = MutableStateFlow(false)
38 | val canRedo = _canRedo.asStateFlow()
39 |
40 | private fun refreshFlags() {
41 | _canUndo.value = undoStack.isNotEmpty()
42 | _canRedo.value = redoStack.isNotEmpty()
43 | }
44 |
45 | suspend fun recordChange(change: ServerListChange) =
46 | mutex.withLock {
47 | undoStack.pushWithLimit(change, maxUndoEntries)
48 | redoStack.clear()
49 | refreshFlags()
50 | logger.debug {
51 | "Recorded change: ${change.description()} " +
52 | "(undo=${undoStack.size}, redo=${redoStack.size})"
53 | }
54 | }
55 |
56 | suspend fun undo(): ServerListChange? =
57 | mutex.withLock {
58 | val lastChange = undoStack.removeLastOrNull() ?: return null
59 | redoStack.pushWithLimit(lastChange, maxUndoEntries)
60 | logger.info {
61 | "Undo: ${lastChange.description()} " +
62 | "(undo=${undoStack.size}, redo=${redoStack.size})"
63 | }
64 | refreshFlags()
65 | lastChange
66 | }
67 |
68 | suspend fun redo(): ServerListChange? =
69 | mutex.withLock {
70 | val redoChange = redoStack.removeLastOrNull() ?: return null
71 | undoStack.pushWithLimit(redoChange, maxUndoEntries)
72 | logger.info {
73 | "Redo: ${redoChange.description()} " +
74 | "(undo=${undoStack.size}, redo=${redoStack.size})"
75 | }
76 | refreshFlags()
77 | redoChange
78 | }
79 |
80 | suspend fun clear() =
81 | mutex.withLock {
82 | undoStack.clear()
83 | redoStack.clear()
84 | refreshFlags()
85 | logger.debug { "Cleared all history" }
86 | }
87 |
88 | private fun ArrayDeque.pushWithLimit(
89 | element: T,
90 | limit: Int,
91 | ) {
92 | if (limit <= 0) return
93 | while (size >= limit) removeFirst()
94 | addLast(element)
95 | }
96 | }
97 |
98 | private val logger = KotlinLogging.logger {}
99 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/widgets/PocketInfoTooltip.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | @file:OptIn(ExperimentalMaterial3Api::class)
20 |
21 | package com.spoiligaming.explorer.ui.widgets
22 |
23 | import androidx.compose.foundation.layout.size
24 | import androidx.compose.foundation.layout.widthIn
25 | import androidx.compose.material.icons.Icons
26 | import androidx.compose.material.icons.outlined.Info
27 | import androidx.compose.material3.ExperimentalMaterial3Api
28 | import androidx.compose.material3.Icon
29 | import androidx.compose.material3.MaterialTheme
30 | import androidx.compose.material3.PlainTooltip
31 | import androidx.compose.material3.Text
32 | import androidx.compose.material3.TooltipAnchorPosition
33 | import androidx.compose.material3.TooltipBox
34 | import androidx.compose.material3.TooltipDefaults.rememberTooltipPositionProvider
35 | import androidx.compose.material3.rememberTooltipState
36 | import androidx.compose.runtime.Composable
37 | import androidx.compose.runtime.LaunchedEffect
38 | import androidx.compose.runtime.getValue
39 | import androidx.compose.runtime.mutableStateOf
40 | import androidx.compose.runtime.remember
41 | import androidx.compose.runtime.setValue
42 | import androidx.compose.ui.Modifier
43 | import androidx.compose.ui.input.pointer.PointerIcon
44 | import androidx.compose.ui.input.pointer.pointerHoverIcon
45 | import androidx.compose.ui.unit.Dp
46 | import androidx.compose.ui.unit.dp
47 | import com.spoiligaming.explorer.ui.extensions.onHover
48 |
49 | @Composable
50 | internal fun PocketInfoTooltip(
51 | text: String,
52 | modifier: Modifier = Modifier,
53 | minWidth: Dp = DefaultTooltipMinWidth,
54 | iconSize: Dp = DefaultIconSize,
55 | ) {
56 | val tooltipState = rememberTooltipState(isPersistent = true)
57 | var isHovered by remember { mutableStateOf(false) }
58 |
59 | LaunchedEffect(isHovered) {
60 | if (isHovered) {
61 | tooltipState.show()
62 | } else {
63 | tooltipState.dismiss()
64 | }
65 | }
66 |
67 | TooltipBox(
68 | positionProvider = rememberTooltipPositionProvider(DefaultTooltipAnchorPosition),
69 | state = tooltipState,
70 | tooltip = {
71 | PlainTooltip(
72 | modifier = Modifier.widthIn(min = minWidth),
73 | ) {
74 | Text(text)
75 | }
76 | },
77 | modifier =
78 | modifier
79 | .onHover { isHovered = it }
80 | .pointerHoverIcon(PointerIcon.Hand),
81 | ) {
82 | Icon(
83 | imageVector = Icons.Outlined.Info,
84 | contentDescription = text,
85 | tint = MaterialTheme.colorScheme.onSurfaceVariant,
86 | modifier = Modifier.size(iconSize),
87 | )
88 | }
89 | }
90 |
91 | private val DefaultTooltipMinWidth = 240.dp
92 | private val DefaultIconSize = 22.dp
93 | private val DefaultTooltipAnchorPosition = TooltipAnchorPosition.Above
94 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/screens/multiplayer/querymethod/QueryMethodConfigurationTooltip.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | @file:OptIn(ExperimentalMaterial3Api::class)
20 |
21 | package com.spoiligaming.explorer.ui.screens.multiplayer.querymethod
22 |
23 | import androidx.compose.foundation.layout.Arrangement
24 | import androidx.compose.foundation.layout.Column
25 | import androidx.compose.foundation.layout.fillMaxWidth
26 | import androidx.compose.foundation.layout.padding
27 | import androidx.compose.material3.Card
28 | import androidx.compose.material3.ExperimentalMaterial3Api
29 | import androidx.compose.material3.HorizontalDivider
30 | import androidx.compose.material3.MaterialTheme
31 | import androidx.compose.material3.RichTooltip
32 | import androidx.compose.material3.Text
33 | import androidx.compose.material3.TooltipScope
34 | import androidx.compose.runtime.Composable
35 | import androidx.compose.ui.Modifier
36 | import androidx.compose.ui.unit.dp
37 | import com.spoiligaming.explorer.ui.screens.multiplayer.querymethod.items.QueryMethodCheckboxItem
38 | import com.spoiligaming.explorer.ui.screens.multiplayer.querymethod.items.QueryMethodTimeoutItem
39 |
40 | @Composable
41 | internal fun TooltipScope.QueryMethodConfigurationTooltip(
42 | title: String,
43 | items: List,
44 | modifier: Modifier = Modifier,
45 | ) = RichTooltip(
46 | title = { Text(text = title) },
47 | text = {
48 | Card(
49 | modifier = modifier.padding(top = TooltipSpacingBetweenTitleAndCard),
50 | ) {
51 | Column(
52 | modifier =
53 | Modifier
54 | .fillMaxWidth()
55 | .padding(TooltipContentPadding),
56 | verticalArrangement = Arrangement.spacedBy(ItemSpacing),
57 | ) {
58 | items.forEachIndexed { index, item ->
59 | when (item) {
60 | is QueryMethodCheckboxSpec ->
61 | QueryMethodCheckboxItem(
62 | spec = item,
63 | modifier = Modifier.fillMaxWidth(),
64 | )
65 |
66 | is TimeoutSliderSpec ->
67 | QueryMethodTimeoutItem(
68 | spec = item,
69 | modifier = Modifier.fillMaxWidth(),
70 | )
71 | }
72 |
73 | if (index < items.lastIndex) {
74 | HorizontalDivider(
75 | thickness = DividerThickness,
76 | color = MaterialTheme.colorScheme.outlineVariant,
77 | )
78 | }
79 | }
80 | }
81 | }
82 | },
83 | )
84 |
85 | private val TooltipSpacingBetweenTitleAndCard = 12.dp
86 | private val TooltipContentPadding = 12.dp
87 | private val ItemSpacing = 8.dp
88 | private val DividerThickness = 0.5.dp
89 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | # Core toolchain
3 | kotlin = "2.3.0"
4 | compose = "1.9.3"
5 | coroutines = "1.10.2"
6 | serialization = "1.9.0"
7 |
8 | # Build & tooling
9 | shadow = "9.3.0"
10 | buildConfig = "6.0.7"
11 | ktlint = "14.0.1"
12 | proguard = "7.8.2"
13 | licenseReport = "3.0.1"
14 |
15 | # UI & Compose ecosystem
16 | colorPicker = "0.7.0"
17 | fileKit = "0.12.0"
18 | materialKolor = "4.0.5"
19 | navigation-compose = "2.9.1"
20 | reorderable = "3.0.0"
21 | autolinktext = "2.0.2"
22 | shimmer = "1.3.3"
23 |
24 | # Networking
25 | ktor = "3.3.3"
26 |
27 | # Logging
28 | kotlin-logging = "7.0.13"
29 | log4j = "2.25.3"
30 |
31 | # System / native
32 | jna = "5.18.1"
33 | oshi = "6.9.2"
34 | themeDetector = "3.9.1"
35 |
36 | # Minecraft
37 | nbt = "6.1"
38 | mcUtils = "3.1.1"
39 |
40 | [libraries]
41 | # Core language/runtime
42 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
43 | compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose" }
44 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", version.ref = "serialization" }
45 | kotlinx-coroutines-core-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "coroutines" }
46 |
47 | # UI & Compose ecosystem
48 | autolinktext = { module = "sh.calvin.autolinktext:autolinktext", version.ref = "autolinktext" }
49 | navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
50 | composeShimmer = { module = "com.valentinilk.shimmer:compose-shimmer", version.ref = "shimmer" }
51 | materialKolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" }
52 | color-picker = { module = "com.godaddy.android.colorpicker:compose-color-picker-jvm", version.ref = "colorPicker" }
53 | reorderable-jvm = { module = "sh.calvin.reorderable:reorderable-jvm", version.ref = "reorderable" }
54 | file-kit = { module = "io.github.vinceglb:filekit-dialogs-compose", version.ref = "fileKit" }
55 |
56 | # Networking
57 | ktor-client-core-jvm = { module = "io.ktor:ktor-client-core-jvm", version.ref = "ktor" }
58 | ktor-client-cio-jvm = { module = "io.ktor:ktor-client-cio-jvm", version.ref = "ktor" }
59 | ktor-client-mock-jvm = { module = "io.ktor:ktor-client-mock-jvm", version.ref = "ktor" }
60 |
61 | # Logging
62 | kotlin-logging = { module = "io.github.oshai:kotlin-logging-jvm", version.ref = "kotlin-logging" }
63 | log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" }
64 | log4j-slf4j2-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" }
65 |
66 | # System / native
67 | jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
68 | jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
69 | oshi-core = { module = "com.github.oshi:oshi-core", version.ref = "oshi" }
70 | theme-detector = { module = "com.github.Dansoftowner:jSystemThemeDetector", version.ref = "themeDetector" }
71 |
72 | # Minecraft
73 | nbt = { module = "com.github.Querz:NBT", version.ref = "nbt" }
74 | mcUtils = { module = "tech.aliorpse.mcutils:mcutils-server-status", version.ref = "mcUtils" }
75 |
76 | [plugins]
77 | # Kotlin toolchain
78 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
79 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
80 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
81 |
82 | # Compose & build tooling
83 | compose = { id = "org.jetbrains.compose", version.ref = "compose" }
84 | shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
85 | buildConfig = { id = "com.github.gmazzo.buildconfig", version.ref = "buildConfig" }
86 | ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
87 | dependencyLicenseReport = { id = "com.github.jk1.dependency-license-report", version.ref = "licenseReport" }
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/screens/settings/components/SettingsSection.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.screens.settings.components
20 |
21 | import androidx.compose.foundation.background
22 | import androidx.compose.foundation.layout.Arrangement
23 | import androidx.compose.foundation.layout.Column
24 | import androidx.compose.foundation.layout.Spacer
25 | import androidx.compose.foundation.layout.fillMaxWidth
26 | import androidx.compose.foundation.layout.height
27 | import androidx.compose.material3.MaterialTheme
28 | import androidx.compose.material3.Surface
29 | import androidx.compose.material3.Text
30 | import androidx.compose.material3.surfaceColorAtElevation
31 | import androidx.compose.runtime.Composable
32 | import androidx.compose.ui.Modifier
33 | import androidx.compose.ui.unit.dp
34 |
35 | @Composable
36 | internal fun SettingsSection(
37 | header: String,
38 | settings: List<@Composable () -> Unit>,
39 | ) = Column(verticalArrangement = Arrangement.spacedBy(OuterArrangement)) {
40 | Text(
41 | text = header,
42 | color = MaterialTheme.colorScheme.onBackground,
43 | style = MaterialTheme.typography.titleLarge,
44 | )
45 |
46 | Column(modifier = Modifier.fillMaxWidth()) {
47 | val containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(SectionContainerElevation)
48 | val dividerColor = MaterialTheme.colorScheme.background
49 |
50 | val sectionOuterShape = MaterialTheme.shapes.large
51 | val sectionInnerShape = MaterialTheme.shapes.extraSmall
52 | val sectionTopShape =
53 | sectionInnerShape.copy(
54 | topStart = sectionOuterShape.topStart,
55 | topEnd = sectionOuterShape.topEnd,
56 | )
57 | val sectionBottomShape =
58 | sectionInnerShape.copy(
59 | bottomStart = sectionOuterShape.bottomStart,
60 | bottomEnd = sectionOuterShape.bottomEnd,
61 | )
62 |
63 | settings.forEachIndexed { index, setting ->
64 | val shape =
65 | when {
66 | settings.size == 1 -> sectionOuterShape
67 | index == 0 -> sectionTopShape
68 | index == settings.lastIndex -> sectionBottomShape
69 | else -> sectionInnerShape
70 | }
71 |
72 | Surface(
73 | modifier = Modifier.fillMaxWidth(),
74 | color = containerColor,
75 | tonalElevation = SectionContainerElevation,
76 | shape = shape,
77 | ) {
78 | setting()
79 | }
80 |
81 | if (index != settings.lastIndex) {
82 | Spacer(
83 | modifier =
84 | Modifier
85 | .fillMaxWidth()
86 | .height(SectionDividerSpacing)
87 | .background(dividerColor),
88 | )
89 | }
90 | }
91 | }
92 | }
93 |
94 | private val OuterArrangement = 8.dp
95 | private val SectionContainerElevation = 8.dp
96 | private val SectionDividerSpacing = 4.dp
97 |
--------------------------------------------------------------------------------
/settings/src/main/kotlin/com/spoiligaming/explorer/settings/util/SettingsFile.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.settings.util
20 |
21 | import io.github.oshai.kotlinlogging.KotlinLogging
22 | import kotlinx.coroutines.Dispatchers
23 | import kotlinx.coroutines.withContext
24 | import kotlinx.serialization.KSerializer
25 | import kotlinx.serialization.json.Json
26 | import java.io.File
27 |
28 | internal class SettingsFile(
29 | private val fileName: String,
30 | private val serializer: KSerializer,
31 | private val defaultValueProvider: () -> T,
32 | ) {
33 | private val json =
34 | Json {
35 | prettyPrint = true
36 | encodeDefaults = true
37 | ignoreUnknownKeys = true
38 | }
39 |
40 | private val settingsFile
41 | get() = SettingsStorage.settingsDir.resolve(fileName)
42 |
43 | val lastModifiedMillis
44 | get() = settingsFile.takeIf { it.exists() }?.lastModified()
45 |
46 | suspend fun read() =
47 | withContext(Dispatchers.IO) {
48 | val dir = SettingsStorage.settingsDir
49 | val file = settingsFile
50 |
51 | logger.debug { "Attempting to read settings from: ${file.absolutePath}" }
52 |
53 | if (!file.exists()) {
54 | ensureDirExists(dir)
55 |
56 | val defaultObj = defaultValueProvider()
57 | file.writeText(json.encodeToString(serializer, defaultObj))
58 |
59 | logger.info { "Created new settings file with default: $defaultObj" }
60 | return@withContext defaultObj
61 | }
62 |
63 | val raw =
64 | file.readText().also {
65 | logger.debug { "Read raw JSON: ${it.take(100).replace("\n", "\\n")}..." }
66 | }
67 |
68 | check(raw.isNotBlank()) {
69 | "SettingsFile.read(): existing file is empty: ${file.absolutePath}"
70 | }
71 |
72 | val loaded = json.decodeFromString(serializer, raw)
73 | logger.info { "Loaded settings from disk: $loaded" }
74 | loaded
75 | }
76 |
77 | suspend fun write(
78 | data: T,
79 | onComplete: (() -> Unit)? = null,
80 | ) = withContext(Dispatchers.IO) {
81 | val dir = SettingsStorage.settingsDir
82 | val file = settingsFile
83 |
84 | ensureDirExists(dir)
85 |
86 | val serialized = json.encodeToString(serializer, data)
87 | file.writeText(serialized)
88 | logger.debug { "Saved settings to disk: $data" }
89 | onComplete?.invoke()
90 | }
91 |
92 | private fun ensureDirExists(dir: File) {
93 | if (!dir.exists()) {
94 | dir.mkdirs().also { created ->
95 | if (created) {
96 | logger.info { "Created settings directory: ${dir.absolutePath}" }
97 | } else {
98 | logger.warn { "Failed to create settings directory: ${dir.absolutePath}" }
99 | }
100 | }
101 | }
102 | }
103 | }
104 |
105 | private val logger = KotlinLogging.logger {}
106 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/screens/multiplayer/querymethod/items/QueryMethodTimeoutItem.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.screens.multiplayer.querymethod.items
20 |
21 | import androidx.compose.foundation.layout.Arrangement
22 | import androidx.compose.foundation.layout.Column
23 | import androidx.compose.foundation.layout.Row
24 | import androidx.compose.foundation.layout.fillMaxWidth
25 | import androidx.compose.material3.MaterialTheme
26 | import androidx.compose.material3.Slider
27 | import androidx.compose.material3.Text
28 | import androidx.compose.runtime.Composable
29 | import androidx.compose.ui.Alignment
30 | import androidx.compose.ui.Modifier
31 | import androidx.compose.ui.unit.dp
32 | import com.spoiligaming.explorer.ui.screens.multiplayer.querymethod.TimeoutSliderSpec
33 | import com.spoiligaming.explorer.ui.t
34 | import com.spoiligaming.explorer.ui.widgets.DebouncedSlider
35 | import com.spoiligaming.explorer.ui.widgets.PocketInfoTooltip
36 | import com.spoiligaming.explorer.ui.widgets.SliderValueAdapters
37 | import server_list_explorer.ui.generated.resources.Res
38 | import server_list_explorer.ui.generated.resources.query_method_timeout_preview_seconds
39 |
40 | @Composable
41 | internal fun QueryMethodTimeoutItem(
42 | spec: TimeoutSliderSpec,
43 | modifier: Modifier = Modifier,
44 | ) = DebouncedSlider(
45 | value = spec.valueSeconds,
46 | onValueChange = spec.onValueChangeSeconds,
47 | valueRange = spec.valueRangeSeconds,
48 | adapter = SliderValueAdapters.IntAdapter,
49 | ) { sliderPosition, onSliderPositionChange, previewSeconds ->
50 | Column(
51 | modifier = modifier,
52 | ) {
53 | Row(
54 | modifier = Modifier.fillMaxWidth(),
55 | verticalAlignment = Alignment.CenterVertically,
56 | horizontalArrangement = Arrangement.SpaceBetween,
57 | ) {
58 | Row(
59 | modifier = Modifier.weight(TITLE_WEIGHT),
60 | verticalAlignment = Alignment.CenterVertically,
61 | horizontalArrangement = Arrangement.spacedBy(TitleSpacing),
62 | ) {
63 | Text(
64 | text = spec.title,
65 | style = MaterialTheme.typography.labelLarge,
66 | color = MaterialTheme.colorScheme.onSurface,
67 | )
68 | if (!spec.description.isNullOrBlank()) {
69 | PocketInfoTooltip(
70 | text = spec.description,
71 | minWidth = TooltipMinWidth,
72 | )
73 | }
74 | }
75 | Text(
76 | text = t(Res.string.query_method_timeout_preview_seconds, previewSeconds),
77 | style = MaterialTheme.typography.labelMedium,
78 | color = MaterialTheme.colorScheme.onSurfaceVariant,
79 | )
80 | }
81 | Slider(
82 | value = sliderPosition,
83 | onValueChange = onSliderPositionChange,
84 | valueRange = spec.valueRangeSeconds,
85 | steps = SLIDER_STEPS,
86 | modifier = Modifier.fillMaxWidth(),
87 | )
88 | }
89 | }
90 |
91 | private val TitleSpacing = 4.dp
92 | private val TooltipMinWidth = 296.dp
93 | private const val TITLE_WEIGHT = 1f
94 | private const val SLIDER_STEPS = 0
95 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/components/MarkdownAutoLinkText.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.components
20 |
21 | import androidx.compose.runtime.Composable
22 | import androidx.compose.runtime.Immutable
23 | import androidx.compose.runtime.remember
24 | import androidx.compose.ui.text.AnnotatedString
25 | import androidx.compose.ui.text.TextLinkStyles
26 | import sh.calvin.autolinktext.SimpleTextMatchResult
27 | import sh.calvin.autolinktext.TextMatcher
28 | import sh.calvin.autolinktext.TextRule
29 | import sh.calvin.autolinktext.TextRuleDefaults
30 | import sh.calvin.autolinktext.rememberAutoLinkText
31 |
32 | @Composable
33 | internal fun rememberAutoLinkMarkdownAnnotatedString(
34 | text: String,
35 | parseMarkdownLinks: Boolean = true,
36 | includeDefaultAutoLinks: Boolean = true,
37 | defaultLinkStyles: TextLinkStyles,
38 | markdownLinkStyles: TextLinkStyles = defaultLinkStyles,
39 | ): AnnotatedString {
40 | val parsed =
41 | remember(text, parseMarkdownLinks) {
42 | if (parseMarkdownLinks) parseMarkdownLinks(text) else MarkdownParseResult(text, emptyList())
43 | }
44 |
45 | val markdownRule =
46 | remember(parsed.matches, markdownLinkStyles) {
47 | TextRule.Url(
48 | textMatcher = TextMatcher.FunctionMatcher { parsed.matches },
49 | styles = markdownLinkStyles,
50 | urlProvider = { it.data },
51 | )
52 | }
53 |
54 | val defaultRules = if (includeDefaultAutoLinks) TextRuleDefaults.defaultList() else emptyList()
55 |
56 | val rules =
57 | remember(markdownRule, includeDefaultAutoLinks, defaultRules) {
58 | buildList> {
59 | add(markdownRule)
60 | addAll(defaultRules)
61 | }
62 | }
63 |
64 | return AnnotatedString.rememberAutoLinkText(
65 | text = parsed.displayText,
66 | textRules = rules.asAnyRules(),
67 | defaultLinkStyles = defaultLinkStyles,
68 | )
69 | }
70 |
71 | private fun parseMarkdownLinks(input: String): MarkdownParseResult {
72 | val out = StringBuilder(input.length)
73 | val matches = ArrayList>()
74 |
75 | var lastIndex = 0
76 | for (m in MarkdownLinkRegex.findAll(input)) {
77 | out.append(input, lastIndex, m.range.first)
78 |
79 | val label = m.groupValues[1]
80 | val url = m.groupValues[2]
81 |
82 | val start = out.length
83 | out.append(label)
84 | val end = out.length
85 |
86 | matches += SimpleTextMatchResult(start, end, url)
87 |
88 | lastIndex = m.range.last + 1
89 | }
90 |
91 | if (lastIndex < input.length) out.append(input, lastIndex, input.length)
92 |
93 | return MarkdownParseResult(
94 | displayText = out.toString(),
95 | matches = matches,
96 | )
97 | }
98 |
99 | @Immutable
100 | private data class MarkdownParseResult(
101 | val displayText: String,
102 | val matches: List>,
103 | )
104 |
105 | @Suppress("UNCHECKED_CAST")
106 | private fun Collection>.asAnyRules() = this as Collection>
107 |
108 | private val MarkdownLinkRegex = Regex("""\[(.+?)]\((https?://[^\s)]+)\)""")
109 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/com/spoiligaming/explorer/minecraft/multiplayer/online/backend/mcsrvstat/McSrvStatServerStatusResponse.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.minecraft.multiplayer.online.backend.mcsrvstat
20 |
21 | import kotlinx.serialization.SerialName
22 | import kotlinx.serialization.Serializable
23 |
24 | @Serializable
25 | internal data class McSrvStatServerStatusResponse(
26 | val ip: String,
27 | val port: Long,
28 | val debug: DebugInfo,
29 | val motd: Motd,
30 | val players: Players,
31 | @SerialName("version")
32 | val versionInfo: String,
33 | val online: Boolean,
34 | val protocol: Protocol,
35 | val hostname: String,
36 | @SerialName("icon")
37 | val rawIcon: String,
38 | val software: String? = null,
39 | val info: Info? = null,
40 | @SerialName("eula_blocked")
41 | val eulaBlocked: Boolean,
42 | ) {
43 | val icon
44 | get() = rawIcon.removePrefix("data:image/png;base64,")
45 | }
46 |
47 | @Serializable
48 | internal data class DebugInfo(
49 | val ping: Boolean,
50 | val query: Boolean,
51 | val bedrock: Boolean,
52 | val srv: Boolean,
53 | val querymismatch: Boolean,
54 | val ipinsrv: Boolean,
55 | val cnameinsrv: Boolean,
56 | val animatedmotd: Boolean,
57 | val cachehit: Boolean,
58 | val cachetime: Long,
59 | val cacheexpire: Long,
60 | val apiversion: Long,
61 | val dns: DnsInfo? = null,
62 | val error: QueryError? = null,
63 | )
64 |
65 | @Serializable
66 | internal data class DnsInfo(
67 | @SerialName("srv")
68 | val srv: List = emptyList(),
69 | @SerialName("srv_a")
70 | val srvA: List = emptyList(),
71 | )
72 |
73 | @Serializable
74 | internal data class DnsRecord(
75 | val name: String? = null,
76 | val type: String? = null,
77 | @SerialName("class")
78 | val recordClass: String? = null,
79 | val ttl: Long? = null,
80 | val rdlength: Long? = null,
81 | val rdata: String? = null,
82 | val priority: Long? = null,
83 | val weight: Long? = null,
84 | val port: Long? = null,
85 | val target: String? = null,
86 | val typecovered: String? = null,
87 | val algorithm: Long? = null,
88 | val labels: Long? = null,
89 | val origttl: Long? = null,
90 | val sigexp: String? = null,
91 | val sigincep: String? = null,
92 | val keytag: Long? = null,
93 | val signname: String? = null,
94 | val signature: String? = null,
95 | val cname: String? = null,
96 | val address: String? = null,
97 | )
98 |
99 | @Serializable
100 | internal data class QueryError(
101 | val query: String,
102 | )
103 |
104 | @Serializable
105 | internal data class Motd(
106 | val raw: List = emptyList(),
107 | val clean: List = emptyList(),
108 | val html: List = emptyList(),
109 | )
110 |
111 | @Serializable
112 | internal data class Players(
113 | val online: Long,
114 | val max: Long,
115 | )
116 |
117 | @Serializable
118 | internal data class Protocol(
119 | val version: Long,
120 | val name: String? = null,
121 | )
122 |
123 | @Serializable
124 | internal data class Info(
125 | val raw: List = emptyList(),
126 | val clean: List = emptyList(),
127 | val html: List = emptyList(),
128 | )
129 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/screens/multiplayer/querymethod/QueryMethodChipPresets.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.screens.multiplayer.querymethod
20 |
21 | import androidx.compose.material.icons.Icons
22 | import androidx.compose.material.icons.outlined.Verified
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.ui.graphics.vector.ImageVector
25 | import com.spoiligaming.explorer.settings.model.ServerQueryMethod
26 | import com.spoiligaming.explorer.ui.t
27 | import org.jetbrains.compose.resources.StringResource
28 | import server_list_explorer.ui.generated.resources.Res
29 | import server_list_explorer.ui.generated.resources.query_method_chip_api_cached
30 | import server_list_explorer.ui.generated.resources.query_method_chip_editors_choice
31 | import server_list_explorer.ui.generated.resources.query_method_chip_editors_choice_badge
32 | import server_list_explorer.ui.generated.resources.query_method_chip_gs4_query_udp
33 | import server_list_explorer.ui.generated.resources.query_method_chip_hosted_api
34 | import server_list_explorer.ui.generated.resources.query_method_chip_srv_support
35 |
36 | private val EditorsChoicePreset =
37 | ChipPreset(
38 | label = Res.string.query_method_chip_editors_choice,
39 | icon =
40 | ChipPresetIcon(
41 | vector = Icons.Outlined.Verified,
42 | contentDescription = Res.string.query_method_chip_editors_choice_badge,
43 | ),
44 | colors = { QueryMethodChipStyles.green(1f) },
45 | )
46 |
47 | private val SrvSupportPreset =
48 | ChipPreset(
49 | label = Res.string.query_method_chip_srv_support,
50 | colors = { QueryMethodChipStyles.common() },
51 | )
52 |
53 | private val QueryUdpPreset =
54 | ChipPreset(
55 | label = Res.string.query_method_chip_gs4_query_udp,
56 | colors = { QueryMethodChipStyles.green(0.8f) },
57 | )
58 |
59 | private val HostedApiPreset =
60 | ChipPreset(
61 | label = Res.string.query_method_chip_hosted_api,
62 | colors = { QueryMethodChipStyles.blue(0.8f) },
63 | )
64 |
65 | private val ApiCachedPreset =
66 | ChipPreset(
67 | label = Res.string.query_method_chip_api_cached,
68 | colors = { QueryMethodChipStyles.yellow(0.5f) },
69 | )
70 |
71 | @Composable
72 | internal fun queryMethodChipsFor(method: ServerQueryMethod) =
73 | when (method) {
74 | ServerQueryMethod.McSrvStat ->
75 | listOf(
76 | HostedApiPreset,
77 | ApiCachedPreset,
78 | QueryUdpPreset,
79 | SrvSupportPreset,
80 | )
81 |
82 | ServerQueryMethod.McUtils ->
83 | listOf(
84 | EditorsChoicePreset,
85 | SrvSupportPreset,
86 | )
87 | }.map { preset ->
88 | val icon =
89 | preset.icon?.let { iconPreset ->
90 | QueryMethodChip.ChipIcon(
91 | vector = iconPreset.vector,
92 | contentDescription = t(iconPreset.contentDescription),
93 | )
94 | }
95 | QueryMethodChip(
96 | label = t(preset.label),
97 | icon = icon,
98 | colors = preset.colors(),
99 | )
100 | }
101 |
102 | private data class ChipPreset(
103 | val label: StringResource,
104 | val icon: ChipPresetIcon? = null,
105 | val colors: @Composable () -> QueryMethodChipColors,
106 | )
107 |
108 | private data class ChipPresetIcon(
109 | val vector: ImageVector,
110 | val contentDescription: StringResource,
111 | )
112 |
--------------------------------------------------------------------------------
/ui/src/main/kotlin/com/spoiligaming/explorer/ui/window/WindowEffects.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Server List Explorer.
3 | * Copyright (C) 2025 SpoilerRules
4 | *
5 | * Server List Explorer is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * Server List Explorer is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with Server List Explorer. If not, see .
17 | */
18 |
19 | package com.spoiligaming.explorer.ui.window
20 |
21 | import androidx.compose.material3.MaterialTheme
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.runtime.LaunchedEffect
24 | import androidx.compose.runtime.State
25 | import androidx.compose.runtime.collectAsState
26 | import androidx.compose.runtime.getValue
27 | import androidx.compose.runtime.mutableStateOf
28 | import androidx.compose.runtime.remember
29 | import androidx.compose.runtime.rememberUpdatedState
30 | import androidx.compose.runtime.setValue
31 | import com.spoiligaming.explorer.settings.manager.windowAppearanceSettingsManager
32 | import com.spoiligaming.explorer.settings.model.TitleBarColorMode
33 | import com.spoiligaming.explorer.ui.extensions.toComposeColor
34 | import com.spoiligaming.explorer.ui.theme.AppTheme
35 | import com.spoiligaming.explorer.ui.window.dwm.dwmStyler
36 | import com.spoiligaming.explorer.util.OSUtils
37 | import kotlinx.coroutines.delay
38 | import javax.swing.JFrame
39 |
40 | internal class WindowEffects(
41 | private val window: JFrame,
42 | ) {
43 | companion object {
44 | private const val DEBOUNCE_MILLIS = 25L
45 | }
46 |
47 | @Composable
48 | fun applyEffects() =
49 | AppTheme {
50 | val windowAppearance by windowAppearanceSettingsManager.settingsFlow.collectAsState()
51 |
52 | val background = MaterialTheme.colorScheme.surface
53 | val outline = MaterialTheme.colorScheme.outlineVariant
54 | val windowCornerPreference = windowAppearance.windowCornerPreference.dwmValue
55 |
56 | val debouncedBackground by rememberDebouncedState(background, DEBOUNCE_MILLIS)
57 | val debouncedOutline by rememberDebouncedState(outline, DEBOUNCE_MILLIS)
58 |
59 | window.dwmStyler().apply {
60 | LaunchedEffect(
61 | windowAppearance.titleBarColorMode,
62 | windowAppearance.customTitleBarColor,
63 | debouncedBackground,
64 | ) {
65 | when (windowAppearance.titleBarColorMode) {
66 | TitleBarColorMode.AUTO -> {
67 | setCaptionColor(debouncedBackground)
68 | }
69 |
70 | TitleBarColorMode.MANUAL -> {
71 | setCaptionColor(windowAppearance.customTitleBarColor.toComposeColor())
72 | }
73 | }
74 | }
75 | LaunchedEffect(
76 | windowAppearance.useCustomBorderColor,
77 | windowAppearance.customBorderColor,
78 | debouncedOutline,
79 | ) {
80 | if (windowAppearance.useCustomBorderColor) {
81 | setBorderColor(windowAppearance.customBorderColor.toComposeColor())
82 | } else {
83 | setBorderColor(debouncedOutline)
84 | }
85 | }
86 | LaunchedEffect(windowCornerPreference) {
87 | if (OSUtils.supportsDwmCornerPreference) {
88 | setCornerPreference(windowCornerPreference)
89 | }
90 | }
91 | }
92 | }
93 | }
94 |
95 | @Composable
96 | private fun rememberDebouncedState(
97 | value: T,
98 | debounceMillis: Long,
99 | ): State {
100 | var debouncedValue by remember { mutableStateOf(value) }
101 |
102 | LaunchedEffect(value) {
103 | delay(debounceMillis)
104 | debouncedValue = value
105 | }
106 |
107 | return rememberUpdatedState(debouncedValue)
108 | }
109 |
--------------------------------------------------------------------------------
/ui/src/main/composeResources/drawable/flag_us.svg:
--------------------------------------------------------------------------------
1 |
57 |
--------------------------------------------------------------------------------