Compare commits

...

22 Commits

Author SHA1 Message Date
Johannes Frohnmeyer e61df73dd3
Merge branch 'gtk_mod_browsing'
ci/woodpecker/push/docs Pipeline is pending Details
ci/woodpecker/push/woodpecker Pipeline failed Details
# Conflicts:
#	build.gradle.kts
#	launcher-gtk/build.gradle.kts
#	launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/GtkMain.kt
#	launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/assistant/KAssistant.kt
#	launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/util/UIExt.kt
#	launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/create/NewInstanceWindow.kt
#	launcher-imgui/src/main/java/io/gitlab/jfronny/inceptum/imgui/window/AddModWindow.java
2023-10-21 17:41:20 +02:00
Johannes Frohnmeyer c94c8b59af
fix: prevent segfault by not exporting account menu.
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/push/docs Pipeline failed Details
Why does this work? No idea. But it seems to fix the crash.
2023-10-21 17:38:55 +02:00
Johannes Frohnmeyer 475717b6b4
chore: use setVisible instead of show 2023-10-21 16:49:49 +02:00
Johannes Frohnmeyer 5cc650921b
fix: prevent instance not exiting setup stage when redownload cancelled 2023-10-21 15:50:35 +02:00
Johannes Frohnmeyer 5b79987dcf
chore: move logging in kotlin code to Log object 2023-10-21 13:35:02 +02:00
Johannes Frohnmeyer 502026bb22
fix: add quickAction suffix 2023-10-21 13:35:02 +02:00
Johannes Frohnmeyer 3746e30ec7
fix: wrap Entry.setText to prevent segfault for empty strings 2023-10-21 13:35:01 +02:00
Johannes Frohnmeyer ad033711f9
chore: basic mod browsing (untested due to GTK4 issues) 2023-10-21 13:35:01 +02:00
Johannes Frohnmeyer f1f2e95dd2
chore: bump javagi 2023-10-21 13:34:57 +02:00
Johannes Frohnmeyer 9e1c20737d
style: don't explicitly depend on gtk transitive dependencies
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-10-16 16:14:46 +02:00
Johannes Frohnmeyer 04d8121ca2
fix: update java-gi and, in doing so, fix segfault 2023-10-16 16:00:57 +02:00
Johannes Frohnmeyer e14294fdd6
chore: perform more actions in the builder stage for simpleDialog
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-08-19 16:20:17 +02:00
Johannes Frohnmeyer 98cc37405a
fix: update AddModWindow for ModrinthApi change 2023-08-19 16:19:50 +02:00
Johannes Frohnmeyer b1b82c423a
fix: prevent segfault in simpleDialog
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-08-19 16:13:45 +02:00
Johannes Frohnmeyer 32a01547c0
build: get rid of toolchains
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-08-19 16:05:07 +02:00
Johannes Frohnmeyer e19d7cfb3b
chore: optimize imports
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-08-19 15:54:27 +02:00
Johannes Frohnmeyer d7ddde6c4d
chore: move logging in kotlin code to Log object
ci/woodpecker/push/docs Pipeline is pending Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-08-19 15:53:09 +02:00
Johannes Frohnmeyer f091cb7a2b
fix: add quickAction suffix
ci/woodpecker/push/docs Pipeline is pending Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-08-19 15:52:24 +02:00
Johannes Frohnmeyer 5faa505235
fix: wrap Entry.setText to prevent segfault for empty strings
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-08-19 15:32:42 +02:00
Johannes Frohnmeyer 8aa0555a2a
fix: use deleteIfExists to work around potential race condition 2023-08-19 15:32:09 +02:00
Johannes Frohnmeyer a7a135c598
chore: basic mod browsing (untested due to GTK4 issues) 2023-07-24 14:29:06 +02:00
Johannes Frohnmeyer 7f168ded43
chore: bump javagi 2023-07-15 22:15:14 +02:00
55 changed files with 636 additions and 221 deletions

View File

@ -3,7 +3,7 @@
pipeline:
export_metadata:
image: gradle:jdk19-jammy
image: gradle:jdk20-jammy
pull: true
commands:
- mkdir public
@ -15,20 +15,20 @@ pipeline:
commands:
- ./platform_jars.sh
build_wrapper:
image: gradle:jdk19-jammy
image: gradle:jdk20-jammy
commands:
- gradle --build-cache :wrapper:build -Pflavor=windows -Ppublic -Ptimestamp=${CI_PIPELINE_STARTED}
- cp wrapper/build/libs/*.exe public/wrapper.exe
- cp wrapper/build/libs/*-all.jar public/wrapper.jar
publish_debug:
image: gradle:jdk19-jammy
image: gradle:jdk20-jammy
commands:
- gradle --build-cache build publish -Pflavor=maven -Ppublic -Ptimestamp=${CI_PIPELINE_STARTED}
secrets: [ maven_token, maven_name ]
when:
- branch: master
publish_release:
image: gradle:jdk19-jammy
image: gradle:jdk20-jammy
commands:
- gradle --build-cache build publish -Pflavor=maven -Ppublic -Prelease
secrets: [ maven_token, maven_name ]

View File

@ -17,7 +17,7 @@ val jbAnnotationsVersion by extra("24.0.1")
val lwjglVersion by extra("3.3.2")
val imguiVersion by extra("1.86.10")
// launcher-gtk
val javagiVersion by extra("v0.5.1")
val javagiVersion by extra("0.7.2")
val flavorProp: String by extra(prop("flavor", "custom"))
if (!setOf("custom", "maven", "fat", "windows", "linux", "macos").contains(flavorProp)) throw IllegalStateException("Unsupported flavor: $flavorProp")

View File

@ -10,5 +10,5 @@ repositories {
dependencies {
implementation("gradle.plugin.com.github.johnrengelman:shadow:7.1.2")
implementation("de.undercouch:gradle-download-task:5.1.2")
implementation("io.gitlab.jfronny:convention:1.4-SNAPSHOT")
implementation("io.gitlab.jfronny:convention:1.5-SNAPSHOT")
}

View File

@ -1,14 +1,8 @@
plugins {
`java-library`
id("jf.java")
`maven-publish`
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(20))
}
}
repositories {
mavenCentral()
maven("https://maven.frohnmeyer-wds.de/artifacts")
@ -42,7 +36,6 @@ afterEvaluate {
if (hasProperty("offline")) {
tasks.withType(JavaExec::class) {
environment("G_ORIGINAL_EXECUTABLE", executable ?: "java")
//TODO once we are not using a toolchain, just do executable(rootDir.resolve("buildSrc/java-offline"))
val originalMetadata = javaLauncher.get().metadata
val field = org.gradle.api.internal.provider.AbstractProperty::class.java.getDeclaredField("value")
field.isAccessible = true

View File

@ -39,7 +39,7 @@ public class Net {
}
public static <T> T downloadObject(String url, ThrowingFunction<String, T, IOException> func, String apiKey) throws IOException {
return downloadObject(url, () -> HttpUtils.get(url).header("x-api-key", apiKey).sendString(), func, true);
return downloadObject(url, () -> downloadStringAuthenticated(url, apiKey), func, true);
}
public static <T> T downloadObject(String url, String sha1, ThrowingFunction<String, T, IOException> func) throws IOException {
@ -82,6 +82,10 @@ public class Net {
return new String(downloadData(url, sha1), StandardCharsets.UTF_8);
}
public static String downloadStringAuthenticated(String url, String apiKey) throws IOException, URISyntaxException {
return HttpUtils.get(url).header("x-api-key", apiKey).sendString();
}
public static void downloadFile(String url, Path path) throws IOException, URISyntaxException {
if (!Files.exists(path.getParent())) Files.createDirectories(path.getParent());
Files.write(path, downloadData(url));

View File

@ -3,28 +3,28 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("inceptum.application")
id("com.github.johnrengelman.shadow")
kotlin("jvm") version "1.9.0-RC"
kotlin("jvm") version "1.9.0"
kotlin("plugin.sam.with.receiver") version "1.9.0"
}
application {
mainClass.set("io.gitlab.jfronny.inceptum.gtk.GtkMain")
}
samWithReceiver {
annotation("io.gitlab.jfronny.commons.SamWithReceiver")
}
repositories {
mavenLocal()
maven("https://jitpack.io") {
content {
includeGroup("com.github.jwharm.java-gi")
}
}
}
dependencies {
val javagiVersion: String by rootProject.extra
implementation("com.github.jwharm.java-gi:glib:$javagiVersion")
implementation("com.github.jwharm.java-gi:gtk:$javagiVersion")
implementation("com.github.jwharm.java-gi:adwaita:$javagiVersion")
//implementation("io.github.jwharm.javagi:glib:$javagiVersion")
//implementation("io.github.jwharm.javagi:gtk:$javagiVersion")
implementation("io.github.jwharm.javagi:adw:$javagiVersion")
implementation(project(":launcher"))
}

View File

@ -1,8 +1,8 @@
package io.gitlab.jfronny.inceptum.gtk
import io.gitlab.jfronny.commons.StringFormatter
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.gtk.util.Log
import io.gitlab.jfronny.inceptum.gtk.window.dialog.MicrosoftLoginDialog
import io.gitlab.jfronny.inceptum.gtk.window.dialog.StringInputDialog
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv.EnvBackend
@ -16,7 +16,7 @@ object GtkEnvBackend : EnvBackend {
var dialogParent: Window? = null
override fun showError(message: String, title: String) {
Utils.LOGGER.error(message)
Log.error(message)
simpleDialog(message, title, null, null)
}
@ -25,12 +25,12 @@ object GtkEnvBackend : EnvBackend {
}
override fun showInfo(message: String, title: String) {
Utils.LOGGER.info(message)
Log.info(message)
simpleDialog(message, title, null, null)
}
override fun showOkCancel(message: String, title: String, ok: Runnable, cancel: Runnable, defaultCancel: Boolean) {
Utils.LOGGER.info(message)
Log.info(message)
simpleDialog(message, title, ok, cancel)
}
@ -74,33 +74,29 @@ object GtkEnvBackend : EnvBackend {
ok: Runnable?,
cancel: Runnable?
) {
val dialog = AlertDialog("")
dialog.message = title
dialog.detail = markup
dialog.modal = true
val dialog = AlertDialog.builder()
.message(title)
.detail(markup)
.modal(true)
when {
cancel == null -> {
dialog.setButtons(arrayOf(I18n["ok"]))
dialog.defaultButton = 0
dialog.cancelButton = -1
}
ok == null -> {
dialog.setButtons(arrayOf("Cancel"))
dialog.defaultButton = -1
dialog.cancelButton = 0
}
else -> {
dialog.setButtons(arrayOf("OK", "Cancel"))
dialog.defaultButton = 0
dialog.cancelButton = 1
}
cancel == null -> dialog.buttons(arrayOf(I18n["ok"]))
.defaultButton(0)
.cancelButton(-1)
ok == null -> dialog.buttons(arrayOf("Cancel"))
.defaultButton(-1)
.cancelButton(0)
else -> dialog.buttons(arrayOf("OK", "Cancel"))
.defaultButton(0)
.cancelButton(1)
}
dialog.choose(parent, Cancellable()) { _, res, _ ->
val result = dialog.chooseFinish(res)
val cancelIdx = dialog.cancelButton
val defaultIdx = dialog.defaultButton
if (result == cancelIdx) cancel?.run()
if (result == defaultIdx) ok?.run()
dialog.build().apply {
choose(parent, Cancellable()) { _, res, _ ->
val result = chooseFinish(res)
val cancelIdx = cancelButton
val defaultIdx = defaultButton
if (result == cancelIdx) cancel?.run()
if (result == defaultIdx) ok?.run()
}
}
}
@ -117,7 +113,7 @@ object GtkEnvBackend : EnvBackend {
}
ResponseType.DELETE_EVENT -> dialog.destroy()
else -> Utils.LOGGER.error("Unexpected response type: $responseId")
else -> Log.error("Unexpected response type: $responseId")
}
}
}

View File

@ -2,7 +2,7 @@ package io.gitlab.jfronny.inceptum.gtk
import io.gitlab.jfronny.inceptum.common.BuildMetadata
import io.gitlab.jfronny.inceptum.common.MetaHolder
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.util.Log
import io.gitlab.jfronny.inceptum.gtk.window.MainWindow
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv
import io.gitlab.jfronny.inceptum.launcher.api.account.AccountManager
@ -19,8 +19,8 @@ object GtkMain {
@JvmStatic
fun main(args: Array<String>) {
LauncherEnv.initialize(GtkEnvBackend)
Utils.LOGGER.info("Launching Inceptum v" + BuildMetadata.VERSION)
Utils.LOGGER.info("Loading from " + MetaHolder.BASE_PATH)
Log.info("Launching Inceptum v" + BuildMetadata.VERSION)
Log.info("Loading from " + MetaHolder.BASE_PATH)
exitProcess(try {
showGui(args)
} catch (_: Throwable) {

View File

@ -3,9 +3,9 @@ package io.gitlab.jfronny.inceptum.gtk
import io.gitlab.jfronny.commons.OSUtils
import io.gitlab.jfronny.commons.io.JFiles
import io.gitlab.jfronny.inceptum.common.MetaHolder
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.menu.MenuBuilder
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.gtk.util.Log
import io.gitlab.jfronny.inceptum.gtk.window.AboutWindow
import io.gitlab.jfronny.inceptum.gtk.window.create.NewInstanceWindow
import io.gitlab.jfronny.inceptum.gtk.window.dialog.MicrosoftLoginDialog
@ -21,8 +21,8 @@ import io.gitlab.jfronny.inceptum.launcher.system.launch.InstanceLauncher
import io.gitlab.jfronny.inceptum.launcher.system.launch.LaunchType
import io.gitlab.jfronny.inceptum.launcher.system.setup.Steps
import io.gitlab.jfronny.inceptum.launcher.util.ProcessState
import org.gnome.gdk.Clipboard
import org.gnome.gio.Cancellable
import org.gnome.gio.Menu
import org.gnome.gtk.*
import java.awt.Toolkit
import java.awt.datatransfer.DataFlavor
@ -68,7 +68,7 @@ object GtkMenubar {
file.button("exit") { app.quit() }
launchMenu = menu.submenu("launch")
generateLaunchMenu(app)
accountsMenu = menu.submenu("account")
accountsMenu = MenuBuilder(app, Menu(), "account") // this should ideally be menu.submenu("account"), but that causes a segfault
generateAccountsMenu(app)
val help = menu.submenu("help")
help.button("about") { AboutWindow.createAndShow() }
@ -80,7 +80,7 @@ object GtkMenubar {
@JvmStatic
fun generateNewMenu(app: Application) {
newMenu!!.clear()
newMenu!!.button("new") { NewInstanceWindow(app).show() }
newMenu!!.button("new") { NewInstanceWindow(app).visible = true }
newMenu!!.button("file") {
val dialog = FileChooserNative(
I18n["menu.file.new.file"],
@ -155,7 +155,7 @@ object GtkMenubar {
}
}
} catch (e: IOException) {
Utils.LOGGER.error("Could not generate launch menu", e)
Log.error("Could not generate launch menu", e)
}
}
@ -182,7 +182,7 @@ object GtkMenubar {
try {
Steps.reDownload(instance, state)
} catch (e: IOException) {
Utils.LOGGER.error("Could not fetch instance, trying to start anyways", e)
Log.error("Could not fetch instance, trying to start anyways", e)
}
if (state.isCancelled) return@show
state.updateStep("Starting Game")
@ -203,11 +203,11 @@ object GtkMenubar {
@JvmStatic
fun generateAccountsMenu(app: Application) {
accountsMenu!!.clear()
accountsMenu!!.button("new") { MicrosoftLoginDialog(GtkEnvBackend.dialogParent).show() }
accountsMenu!!.button("new") { MicrosoftLoginDialog(GtkEnvBackend.dialogParent).visible = true }
accountsMenu!!.button("manage") {
val window = LauncherSettingsWindow(app)
window.activePage = "settings.accounts"
window.show()
window.visible = true
}
val accounts: MutableList<MicrosoftAccount?> = ArrayList(AccountManager.getAccounts())
accounts.add(null)

View File

@ -1,6 +1,6 @@
package io.gitlab.jfronny.inceptum.gtk
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.util.Log
import java.util.*
private val SCHEDULED: Queue<Runnable> = ArrayDeque()
@ -15,7 +15,7 @@ fun runScheduledTasks() {
try {
r!!.run()
} catch (t: Throwable) {
Utils.LOGGER.error("Could not run scheduled task", t)
Log.error("Could not run scheduled task", t)
}
}
}

View File

@ -3,8 +3,8 @@ package io.gitlab.jfronny.inceptum.gtk.control
import io.gitlab.jfronny.inceptum.gtk.util.get
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
import org.gnome.gtk.*
import org.pango.EllipsizeMode
import org.pango.WrapMode
import org.gnome.pango.EllipsizeMode
import org.gnome.pango.WrapMode
class InstanceGridEntryFactory(
private val instanceList: List<Instance>

View File

@ -16,6 +16,7 @@ import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceNameTool
import io.gitlab.jfronny.inceptum.launcher.system.launch.LaunchType
import org.gnome.adw.ActionRow
import org.gnome.gdk.Gdk
import org.gnome.gio.Menu
import org.gnome.gtk.*
import java.io.IOException
@ -32,7 +33,6 @@ class InstanceListEntryFactory(
launch.addCssClass("flat")
launch.name = "inceptum-launch"
launch.tooltipText = I18n["instance.launch"]
launch.hasTooltip = true
val menu = MenuButton()
menu.addCssClass("flat")
@ -49,7 +49,7 @@ class InstanceListEntryFactory(
row.fixSubtitle()
val rightClicked = GestureClick()
rightClicked.button = 3
rightClicked.button = Gdk.BUTTON_SECONDARY
rightClicked.onPressed { nPress, _, _ -> if (nPress == 1) menu.emitActivate() }
row.addController(rightClicked)
@ -59,11 +59,11 @@ class InstanceListEntryFactory(
override fun BindContext.bind(widget: ActionRow, data: Decomposed) {
if (data.instance?.isLocked ?: true) {
data.item.activatable = false
data.row.subtitle = if (data.instance?.isRunningLocked ?: false) I18n["instance.launch.locked.running"]
widget.subtitle = if (data.instance?.isRunningLocked ?: false) I18n["instance.launch.locked.running"]
else I18n["instance.launch.locked.setup"]
}
data.row.title = data.instance.toString()
widget.title = data.instance.toString()
data.thumbnail.bind(data.instance!!)
@ -137,14 +137,13 @@ class InstanceListEntryFactory(
val launch = suffixes.firstChild as Button
val menuButton = launch.nextSibling as MenuButton
val popoverMenu = menuButton.popover as PopoverMenu
return Decomposed(listItem, id, instance, widget, thumbnail, launch, popoverMenu)
return Decomposed(listItem, id, instance, thumbnail, launch, popoverMenu)
}
class Decomposed(
val item: ListItem,
val instanceId: String,
val instance: Instance?,
val row: ActionRow,
val thumbnail: InstanceThumbnail,
val launch: Button,
val popoverMenu: PopoverMenu

View File

@ -6,7 +6,6 @@ import org.gnome.gtk.PropertyExpression
import org.gnome.gtk.StringList
import org.gnome.gtk.StringObject
import org.jetbrains.annotations.PropertyKey
import java.util.ArrayList
import java.util.function.IntConsumer
class KDropDown<T>(options: Array<T>, private val stringify: (T) -> String, selected: Int): DropDown(options.toModel(stringify), null) {

View File

@ -1,5 +1,6 @@
package io.gitlab.jfronny.inceptum.gtk.control
import io.gitlab.jfronny.inceptum.gtk.util.kText
import org.gnome.gtk.Entry
import java.util.function.Consumer
@ -7,7 +8,7 @@ class KEntry(value: String? = ""): Entry() {
private val onChange = ArrayList<Consumer<String>>()
init {
text = value ?: ""
kText = value ?: ""
onChanged { onChange.forEach { it.accept(text) } }
}

View File

@ -1,13 +1,13 @@
package io.gitlab.jfronny.inceptum.gtk.control
import io.github.jwharm.javagi.base.Signal
import io.github.jwharm.javagi.gobject.SignalConnection
import org.gnome.gtk.ListItem
import org.gnome.gtk.SignalListItemFactory
import org.gnome.gtk.StringObject
import org.gnome.gtk.Widget
abstract class KSignalListItemFactory<TData, TWidget : Widget> : SignalListItemFactory() {
private val toDisconnect: MutableMap<String, MutableSet<Signal<*>>> = HashMap()
private val toDisconnect: MutableMap<String, MutableSet<SignalConnection<*>>> = HashMap()
init {
onSetup {
val li = it as ListItem
@ -41,14 +41,14 @@ abstract class KSignalListItemFactory<TData, TWidget : Widget> : SignalListItemF
}
interface BindContext: ActionContext {
fun registerForUnbind(signal: Signal<*>)
fun registerForUnbind(signal: SignalConnection<*>)
}
interface UnbindContext: ActionContext {
}
private inner class BindContextImpl(private val id: String, override val listItem: ListItem) : BindContext {
override fun registerForUnbind(signal: Signal<*>) {
override fun registerForUnbind(signal: SignalConnection<*>) {
toDisconnect.computeIfAbsent(id) { _ -> HashSet() }
.add(signal)
}

View File

@ -0,0 +1,37 @@
package io.gitlab.jfronny.inceptum.gtk.control
import org.gnome.gtk.Align
import org.gnome.gtk.ProgressBar
import org.gnome.gtk.Revealer
import org.gnome.gtk.RevealerTransitionType
class LoadingRevealer: Revealer() {
private val progressBar: ProgressBar
init {
transitionType = RevealerTransitionType.CROSSFADE
revealChild = false
vexpand = false
hexpand = false
valign = Align.CENTER
halign = Align.FILL
progressBar = ProgressBar().apply {
hexpand = true
vexpand = false
addCssClass("osd")
}
child = progressBar
}
fun setRunning(state: Boolean) {
revealChild = state
}
fun setProgress(progress: Double) {
progressBar.fraction = progress.coerceIn(0.0, 1.0)
}
fun pulse() {
progressBar.pulse()
}
}

View File

@ -1,6 +1,6 @@
package io.gitlab.jfronny.inceptum.gtk.control.assistant
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.util.Log
import org.gnome.gtk.Application
import org.gnome.gtk.Assistant
import org.gnome.gtk.AssistantPageType
@ -13,7 +13,7 @@ open class KAssistant(app: Application) : Assistant() {
onPrepare { next ->
val page = pages.firstOrNull { it.handle() == next.handle() }
if (page == null) {
Utils.LOGGER.error("Unknown page opened in assistant")
Log.error("Unknown page opened in assistant")
} else {
page.emitOpen()
}

View File

@ -7,7 +7,7 @@ import org.gnome.gtk.*
import org.jetbrains.annotations.PropertyKey
import java.util.concurrent.atomic.AtomicInteger
open class SectionedSettingsTab(window: Window?): SettingsTab<Box>(window, Box(Orientation.VERTICAL, 8)) {
open class SectionedSettingsTab<W : Window>(window: W?): SettingsTab<Box, W>(window, Box(Orientation.VERTICAL, 8)) {
init {
content.marginHorizontal = 24
content.marginTop = 12

View File

@ -5,8 +5,8 @@ import io.gitlab.jfronny.inceptum.gtk.GtkEnvBackend
import org.gnome.gtk.Widget
import org.gnome.gtk.Window
open class SettingsTab<T : Widget>(
protected val window: Window?,
open class SettingsTab<T : Widget, W : Window>(
protected val window: W?,
val content: T
) {
protected fun showError(message: String, t: Throwable) =

View File

@ -1,6 +1,8 @@
package io.gitlab.jfronny.inceptum.gtk.control.settings
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.gtk.util.Log
import org.gnome.adw.HeaderBar
import org.gnome.adw.ViewStack
import org.gnome.adw.ViewSwitcherBar
@ -40,7 +42,7 @@ open class SettingsWindow(app: Application?) : Window() {
setDefaultSize(720, 360)
}
fun addTab(tab: SettingsTab<*>, title: @PropertyKey(resourceBundle = I18n.BUNDLE) String, iconName: String) {
fun addTab(tab: SettingsTab<*, *>, title: @PropertyKey(resourceBundle = I18n.BUNDLE) String, iconName: String) {
stack.addTitledWithIcon(tab.content, title, I18n[title], iconName)
}

View File

@ -1,11 +1,11 @@
package io.gitlab.jfronny.inceptum.gtk.menu
import io.github.jwharm.javagi.glib.types.VariantTypes
import io.gitlab.jfronny.commons.throwable.ThrowingRunnable
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.gtk.util.Log
import org.gnome.gio.*
import org.gnome.glib.Variant
import org.gnome.glib.VariantType
import org.gnome.gtk.Application
import org.gnome.gtk.MenuButton
import org.gnome.gtk.PopoverMenu
@ -51,7 +51,7 @@ class MenuBuilder private constructor(map: ActionMap, menu: Menu, prefix: String
try {
onClick.run()
} catch (e: Throwable) {
Utils.LOGGER.error("Could not execute action", e)
Log.error("Could not execute action", e)
}
}
val menuItem = MenuItem(label, groupName + internalName)
@ -88,7 +88,7 @@ class MenuBuilder private constructor(map: ActionMap, menu: Menu, prefix: String
): BuiltRadioItem<T> {
var name = name
name = prefix + name
val action = SimpleAction.newStateful(name, VariantType("i"), Variant.newInt32(options.indexOf(initial)))
val action = SimpleAction.newStateful(name, VariantTypes.INT32, Variant.newInt32(options.indexOf(initial)))
addAction(name, action)
action.onActivate { variant: Variant? ->
action.state = variant

View File

@ -0,0 +1,6 @@
package io.gitlab.jfronny.inceptum.gtk.util
import io.gitlab.jfronny.commons.log.Logger
import io.gitlab.jfronny.inceptum.common.Utils
object Log : Logger by Utils.LOGGER

View File

@ -1,7 +1,6 @@
package io.gitlab.jfronny.inceptum.gtk.util
import io.gitlab.jfronny.commons.OSUtils
import io.gitlab.jfronny.inceptum.common.Utils
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
@ -35,12 +34,12 @@ object Memory {
return if (memTotal.isPresent()) {
parseDecimalMemorySizeToBinary(memTotal.get())
} else {
Utils.LOGGER.error("Could not find total memory")
Log.error("Could not find total memory")
32 * GB
}
}
} catch (e: IOException) {
Utils.LOGGER.error("Could not get total memory", e)
Log.error("Could not get total memory", e)
return 32 * GB
}
}

View File

@ -5,5 +5,6 @@ import org.gnome.gtk.StringList
fun StringList.clear() = splice(0, size, null)
fun StringList.addAll(values: Array<String>) = splice(size, 0, values)
fun StringList.replaceAll(values: Array<String>) = splice(0, size, values)
val ListModel.size get() = nItems

View File

@ -2,9 +2,7 @@ package io.gitlab.jfronny.inceptum.gtk.util
import io.gitlab.jfronny.inceptum.gtk.control.ILabel
import org.gnome.adw.ActionRow
import org.gnome.gtk.Label
import org.gnome.gtk.MessageDialog
import org.gnome.gtk.Widget
import org.gnome.gtk.*
var Widget.margin: Int
set(value) {
@ -31,4 +29,6 @@ var MessageDialog.markup: String
set(value) { setMarkup(value) }
get() = throw NotImplementedError()
fun ActionRow.fixSubtitle() = ILabel.theme(firstChild!!.lastChild!!.prevSibling!!.lastChild as Label, ILabel.Mode.SUBTITLE)
fun ActionRow.fixSubtitle() = ILabel.theme(firstChild!!.lastChild!!.prevSibling!!.lastChild as Label, ILabel.Mode.SUBTITLE)
fun EntryBuffer.clear() = deleteText(0, length)

View File

@ -25,7 +25,7 @@ class AboutWindow : AboutDialog() {
companion object {
@JvmStatic
fun createAndShow() {
AboutWindow().show()
AboutWindow().visible = true
}
}
}

View File

@ -54,7 +54,7 @@ class MainWindow(app: Application) : ApplicationWindow(app) {
val uiMenu = MenuBuilder(app, Menu(), "hamburger")
uiMenu.button("support") { Utils.openWebBrowser(URI("https://git.frohnmeyer-wds.de/JfMods/Inceptum/issues")) }
uiMenu.button("preferences") { LauncherSettingsWindow(app).show() }
uiMenu.button("preferences") { LauncherSettingsWindow(app).visible = true }
uiMenu.button("about") { AboutWindow.createAndShow() }
val menuButton = MenuButton()
menuButton.iconName = "open-menu-symbolic"
@ -112,7 +112,7 @@ class MainWindow(app: Application) : ApplicationWindow(app) {
try {
setupDirWatcher()
} catch (e: IOException) {
Utils.LOGGER.error(
Log.error(
"Could not set up watch service, live updates of the instance dir will be unavailable",
e
)
@ -126,7 +126,7 @@ class MainWindow(app: Application) : ApplicationWindow(app) {
try {
if (isw.poll()) generateWindowBody()
} catch (e: IOException) {
Utils.LOGGER.error("Could not run update task", e)
Log.error("Could not run update task", e)
}
GLib.SOURCE_CONTINUE
}
@ -160,7 +160,7 @@ class MainWindow(app: Application) : ApplicationWindow(app) {
stack.queueResize()
stack.queueDraw()
} catch (e: IOException) {
Utils.LOGGER.error("Could not generate window body", e)
Log.error("Could not generate window body", e)
}
}
}

View File

@ -1,15 +1,13 @@
package io.gitlab.jfronny.inceptum.gtk.window.create
import io.gitlab.jfronny.commons.StringFormatter
import io.gitlab.jfronny.commons.io.JFiles
import io.gitlab.jfronny.inceptum.common.InceptumConfig
import io.gitlab.jfronny.inceptum.common.MetaHolder
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.control.KDropDown
import io.gitlab.jfronny.inceptum.gtk.control.KEntry
import io.gitlab.jfronny.inceptum.gtk.control.assistant.KAssistant
import io.gitlab.jfronny.inceptum.gtk.schedule
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.gtk.util.Log
import io.gitlab.jfronny.inceptum.gtk.util.toTypedArray
import io.gitlab.jfronny.inceptum.gtk.window.dialog.ProcessStateWatcherDialog
import io.gitlab.jfronny.inceptum.launcher.api.FabricMetaApi
@ -22,7 +20,6 @@ import io.gitlab.jfronny.inceptum.launcher.system.setup.SetupStepInfo
import io.gitlab.jfronny.inceptum.launcher.system.setup.Steps
import org.gnome.glib.GLib
import org.gnome.gtk.*
import java.io.IOException
class NewInstanceWindow(app: Application) : KAssistant(app) {
@ -143,21 +140,19 @@ class NewInstanceWindow(app: Application) : KAssistant(app) {
try {
for (step in Steps.STEPS) {
if (state.isCancelled) {
try {
JFiles.deleteRecursive(MetaHolder.INSTANCE_DIR.resolve(state.name))
} catch (e: IOException) {
Utils.LOGGER.error("Could not delete instance dir", e)
}
state.tryRemoveInstance()
return@Thread
}
pState.incrementStep(step.name)
step.execute(state)
}
state.clearSetupLock()
} catch (e: Throwable) {
pState.cancel()
Utils.LOGGER.error("Could not create instance")
Log.error("Could not create instance")
failureMessage = StringFormatter.toString(e)
isFailure = true
state.tryRemoveInstance()
} finally {
finished = true
schedule { setComplete(true) }

View File

@ -2,6 +2,7 @@ package io.gitlab.jfronny.inceptum.gtk.window.dialog
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.gtk.util.Log
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAccount
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAuthAPI
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAuthServer
@ -28,7 +29,7 @@ class MicrosoftLoginDialog(
try {
server.start()
} catch (e: Exception) {
Utils.LOGGER.error("Could not start mc login server", e)
Log.error("Could not start mc login server", e)
}
val finalize = Runnable {
server.close()
@ -46,7 +47,7 @@ class MicrosoftLoginDialog(
destroy()
}
else -> Utils.LOGGER.error("Unexpected response type: $responseId")
else -> Log.error("Unexpected response type: $responseId")
}
}
val btn = Button.newWithLabel(I18n["auth.open-browser"])
@ -55,7 +56,7 @@ class MicrosoftLoginDialog(
try {
Utils.openWebBrowser(URI(MicrosoftAuthAPI.MICROSOFT_LOGIN_URL))
} catch (e: URISyntaxException) {
Utils.LOGGER.error("Could not open browser", e)
Log.error("Could not open browser", e)
}
}
onCloseRequest {

View File

@ -2,10 +2,10 @@ package io.gitlab.jfronny.inceptum.gtk.window.dialog
import io.gitlab.jfronny.commons.StringFormatter
import io.gitlab.jfronny.commons.throwable.ThrowingRunnable
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.GtkEnvBackend
import io.gitlab.jfronny.inceptum.gtk.schedule
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.gtk.util.Log
import io.gitlab.jfronny.inceptum.launcher.util.ProcessState
import org.gnome.glib.GLib
import org.gnome.gtk.*
@ -39,7 +39,7 @@ class ProcessStateWatcherDialog(
}
ResponseType.DELETE_EVENT -> destroy()
else -> Utils.LOGGER.error("Unexpected response type: $responseId")
else -> Log.error("Unexpected response type: $responseId")
}
}
onCloseRequest {
@ -65,7 +65,7 @@ class ProcessStateWatcherDialog(
executor.run()
} catch (e: Throwable) {
state.cancel()
Utils.LOGGER.error(errorMessage, e)
Log.error(errorMessage, e)
GtkEnvBackend.simpleDialog(
parent,
StringFormatter.toString(e),
@ -95,7 +95,7 @@ class ProcessStateWatcherDialog(
executor: ThrowingRunnable<*>
): ProcessStateWatcherDialog {
val dialog = ProcessStateWatcherDialog(parent, title, errorMessage, state, executor)
dialog.show()
dialog.visible = true
return dialog
}
}

View File

@ -1,5 +1,6 @@
package io.gitlab.jfronny.inceptum.gtk.window.dialog
import io.gitlab.jfronny.inceptum.gtk.util.kText
import org.gnome.gtk.*
class StringInputDialog(parent: Window?, flags: DialogFlags, type: MessageType, buttons: ButtonsType, message: String, value: String) : MessageDialog(parent, flags, type, buttons, message) {
@ -7,7 +8,7 @@ class StringInputDialog(parent: Window?, flags: DialogFlags, type: MessageType,
init {
(messageArea as Box).append(entry)
entry.text = value
entry.kText = value
}
val input: String get() = entry.text

View File

@ -13,7 +13,8 @@ import org.gnome.gio.Cancellable
import org.gnome.gtk.*
import java.nio.file.Path
class ExportTab(private val instance: Instance, window: InstanceSettingsWindow?) : SectionedSettingsTab(window) {
class ExportTab(window: InstanceSettingsWindow) : SectionedSettingsTab<InstanceSettingsWindow>(window) {
private val instance: Instance = window.instance
init {
section(null) {
row("instance.settings.export.version", "instance.settings.export.version.subtitle") {

View File

@ -8,13 +8,9 @@ import io.gitlab.jfronny.inceptum.common.MetaHolder
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.control.ILabel
import io.gitlab.jfronny.inceptum.gtk.control.settings.SectionedSettingsTab
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.gtk.util.Memory
import io.gitlab.jfronny.inceptum.gtk.util.markup
import io.gitlab.jfronny.inceptum.gtk.util.toTypedArray
import io.gitlab.jfronny.inceptum.gtk.util.*
import io.gitlab.jfronny.inceptum.launcher.api.FabricMetaApi
import io.gitlab.jfronny.inceptum.launcher.api.McApi
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceList
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceNameTool
import io.gitlab.jfronny.inceptum.launcher.util.GameVersionParser
@ -26,12 +22,13 @@ import java.nio.file.Files
import java.nio.file.Path
import java.util.*
class GeneralTab(instance: Instance, window: InstanceSettingsWindow) : SectionedSettingsTab(window) {
class GeneralTab(window: InstanceSettingsWindow) : SectionedSettingsTab<InstanceSettingsWindow>(window) {
companion object {
private val VERSIONS = McApi.getVersions()
}
init {
val instance = window.instance
section(null) {
row("instance.settings.general.name", "instance.settings.general.name.placeholder") {
val apply = Button.newWithLabel(I18n["instance.settings.apply"])
@ -43,7 +40,7 @@ class GeneralTab(instance: Instance, window: InstanceSettingsWindow) : Sectioned
val newPath = MetaHolder.INSTANCE_DIR.resolve(InstanceNameTool.getNextValid(entry.text))
Files.move(instance.path, newPath)
window.close()
InstanceSettingsWindow(window.application, InstanceList.read(newPath)).show()
InstanceSettingsWindow(window.application, InstanceList.read(newPath)).visible = true
} catch (e: IOException) {
showError("Could not rename", e)
}
@ -144,13 +141,13 @@ class GeneralTab(instance: Instance, window: InstanceSettingsWindow) : Sectioned
try {
dialog.setFile(File.newForPath(instance.meta.java))
} catch (e: GErrorException) {
Utils.LOGGER.error("Could not set starting point", e)
Log.error("Could not set starting point", e)
}
}
dialog.onResponse { responseId: Int ->
if (responseId == ResponseType.ACCEPT.value) {
val file = dialog.file!!.path
if (file != null) entry.text = file
if (file != null) entry.kText = file
}
}
dialog.show()
@ -233,7 +230,7 @@ class GeneralTab(instance: Instance, window: InstanceSettingsWindow) : Sectioned
else -> {}
}
}
dialog.show()
dialog.visible = true
}
}
row("instance.directory", "instance.directory.subtitle") {

View File

@ -4,10 +4,10 @@ import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsWindow
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
import org.gnome.gtk.Application
class InstanceSettingsWindow(app: Application?, instance: Instance) : SettingsWindow(app) {
class InstanceSettingsWindow(val app: Application?, val instance: Instance) : SettingsWindow(app) {
init {
addTab(GeneralTab(instance, this), "instance.settings.general", "preferences-other-symbolic")
addTab(ModsTab(instance, this), "instance.settings.mods", "package-x-generic-symbolic")
addTab(ExportTab(instance, this), "instance.settings.export", "send-to-symbolic")
addTab(GeneralTab(this), "instance.settings.general", "preferences-other-symbolic")
addTab(ModsTab(this), "instance.settings.mods", "package-x-generic-symbolic")
addTab(ExportTab(this), "instance.settings.export", "send-to-symbolic")
}
}

View File

@ -1,20 +1,55 @@
package io.gitlab.jfronny.inceptum.gtk.window.settings.instance
import io.gitlab.jfronny.commons.concurrent.AsyncRequest
import io.gitlab.jfronny.commons.concurrent.VoidFuture
import io.gitlab.jfronny.inceptum.gtk.control.ILabel
import io.gitlab.jfronny.inceptum.gtk.control.KDropDown
import io.gitlab.jfronny.inceptum.gtk.control.KSignalListItemFactory
import io.gitlab.jfronny.inceptum.gtk.control.LoadingRevealer
import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsTab
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.gtk.util.clear
import io.gitlab.jfronny.inceptum.gtk.util.fixSubtitle
import io.gitlab.jfronny.inceptum.gtk.util.replaceAll
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv
import io.gitlab.jfronny.inceptum.launcher.api.CurseforgeApi
import io.gitlab.jfronny.inceptum.launcher.api.ModrinthApi
import io.gitlab.jfronny.inceptum.launcher.model.modrinth.ModrinthProjectType
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
import io.gitlab.jfronny.inceptum.launcher.system.instance.Mod
import io.gitlab.jfronny.inceptum.launcher.system.instance.ModManager
import io.gitlab.jfronny.inceptum.launcher.system.instance.ModPath
import io.gitlab.jfronny.inceptum.launcher.system.mds.ModsDirScanner
import io.gitlab.jfronny.inceptum.launcher.system.source.CurseforgeModSource
import io.gitlab.jfronny.inceptum.launcher.system.source.ModSource
import io.gitlab.jfronny.inceptum.launcher.system.source.ModrinthModSource
import org.gnome.adw.ActionRow
import org.gnome.adw.Leaflet
import org.gnome.adw.LeafletTransitionType
import org.gnome.gtk.Box
import org.gnome.gtk.Orientation
import org.gnome.gtk.SearchBar
import org.gnome.gtk.SearchEntry
import org.gnome.gtk.Separator
import org.gnome.glib.GLib
import org.gnome.gtk.*
import org.jetbrains.annotations.PropertyKey
import java.util.concurrent.ForkJoinPool
import kotlin.jvm.optionals.getOrNull
class ModsTab(window: InstanceSettingsWindow) : SettingsTab<Leaflet, InstanceSettingsWindow>(window, Leaflet()) {
private val instance: Instance
private val mds: ModsDirScanner
private val listModel: StringList
private val loadingRevealer = LoadingRevealer()
private val descriptionLabel: ILabel
private var page: Page = Page.LOCAL
enum class Page {
LOCAL, MODRINTH, CURSEFORGE
}
class ModsTab(instance: Instance?, window: InstanceSettingsWindow?) : SettingsTab<Leaflet>(window, Leaflet()) {
init {
instance = window.instance
mds = instance.mds
content.apply {
//TODO consider filter panel via Flap
homogeneous = false
canNavigateBack = true
transitionType = LeafletTransitionType.OVER
@ -23,9 +58,9 @@ class ModsTab(instance: Instance?, window: InstanceSettingsWindow?) : SettingsTa
append(KDropDown("instance.settings.mods.local", "instance.settings.mods.modrinth", "instance.settings.mods.curseforge").apply {
onChange { newFilter ->
when (newFilter) {
0 -> switchToLocalMods()
1 -> switchToModrinthMods()
2 -> switchToCurseforgeMods()
0 -> switchTo(Page.LOCAL)
1 -> switchTo(Page.MODRINTH)
2 -> switchTo(Page.CURSEFORGE)
}
}
})
@ -39,17 +74,207 @@ class ModsTab(instance: Instance?, window: InstanceSettingsWindow?) : SettingsTa
child = entry
keyCaptureWidget = entry
})
listModel = StringList(arrayOf())
append(Overlay().apply {
child = ModsListView(listModel)
addOverlay(loadingRevealer)
})
})
append(Separator(Orientation.VERTICAL)).navigatable = false
append(Box(Orientation.VERTICAL, 6).apply {
append(ILabel("instance.settings.mods.unsupported"))
descriptionLabel = ILabel("instance.settings.mods.select")
append(descriptionLabel)
//TODO content
})
addTickCallback { _, _ ->
val toShow = mutableListOf<String>()
if (page == Page.LOCAL) {
loadingRevealer.setRunning(!mds.isComplete)
val mods = window.instance.mods
loadingRevealer.setProgress((mods.filter { mds.hasScanned(it) }.size.toDouble() / mods.size))
for (mod in mods) {
if (mod.name.contains(currentSearchString)) {
//TODO improve this search
this@ModsTab.mods[mod.name] = ModState.Installed(mod)
toShow.add(mod.name)
}
}
listModel.replaceAll(mods.map { it.name }.toTypedArray())
} else {
loadingRevealer.setRunning(searchResult == null || !mds.isComplete)
if (searchResult != null) {
for (mod in searchResult.orEmpty()) {
this@ModsTab.mods[mod.name] = mod
toShow.add(mod.name)
}
}
}
listModel.replaceAll(toShow.toTypedArray())
GLib.SOURCE_CONTINUE
}
}
}
fun switchToLocalMods(): Unit = TODO("set up")
fun switchToModrinthMods(): Unit = TODO("set up")
fun switchToCurseforgeMods(): Unit = TODO("set up")
fun updateSearch(search: String): Unit = TODO("set up")
private var currentSearchString: String = ""
private var searchResult: List<ModState>? = null
//TODO search pagination
private val search = AsyncRequest({
searchResult = null
loadingRevealer.setRunning(true)
loadingRevealer.pulse()
VoidFuture(ForkJoinPool.commonPool().submit {
val sources: List<ModSource> = when (page) {
Page.CURSEFORGE -> {
val cf = CurseforgeApi.search(instance.gameVersion, currentSearchString, 0, "Popularity") //TODO allow user to choose sort mode
cf.mapNotNull {
if (isCancelled) return@submit
val file = it.latestFilesIndexes.firstOrNull { it.gameVersion == instance.gameVersion } //TODO support compatible minor versions
if (file == null) null
else CurseforgeModSource(it.id, file.fileId)
}
}
Page.MODRINTH -> {
val mr = ModrinthApi.search(currentSearchString, 0, instance.gameVersion, ModrinthProjectType.mod).hits
mr.mapNotNull {
if (isCancelled) return@submit
val file = ModrinthApi.getLatestVersions(it.project_id, instance.gameVersion).best
if (file == null) null
else ModrinthModSource(file.id)
}
}
Page.LOCAL -> listOf()
}
searchResult = sources.map { ModState(it) }
})
}, {
loadingRevealer.setRunning(false)
})
fun switchTo(page: Page): Unit {
listModel.clear()
mods.clear()
this.page = page
updateSearch(currentSearchString)
}
fun updateSearch(search: String): Unit {
descriptionLabel.text = "Searching is currently unsupported"
currentSearchString = search
this.search.request()
}
fun selectMod(mod: ModState): Unit {
//TODO detailed menu for version selection, ...
descriptionLabel.setMarkup(mod.description)
}
inner class ModsListView(model: StringList): ListView(ModsListSelectionModel(model), ModsListItemFactory()) {
init {
vscrollPolicy = ScrollablePolicy.NATURAL
addCssClass("navigation-sidebar")
}
}
class ModsListSelectionModel(model: StringList): SingleSelection(model) {
init {
autoselect = false
// onSelectionChanged { position, nItems ->
// val v = (selectedItem as? StringObject)?.string
// if (v != null)
// }
}
}
private val mods = LinkedHashMap<String, ModState>()
//TODO https://gitlab.gnome.org/World/gfeeds/-/blob/master/data/ui/sidebar_listbox_row.blp
inner class ModsListItemFactory: KSignalListItemFactory<Decomposed, ActionRow>() {
override fun setup(): ActionRow {
val row = ActionRow()
row.activatable = true
val quickAction = Button.newFromIconName("folder-download-symbolic")
quickAction.addCssClass("flat")
quickAction.tooltipText = I18n["instance.settings.mods.download"]
row.addSuffix(quickAction)
row.fixSubtitle()
return row
}
override fun BindContext.bind(widget: ActionRow, data: Decomposed) {
widget.title = data.mod!!.name
widget.subtitle = data.mod.summary
registerForUnbind(widget.onActivated { selectMod(data.mod) })
fun setupQuickAction(
iconName: String,
description: @PropertyKey(resourceBundle = I18n.BUNDLE) String,
handler: Button.Clicked
) {
data.quickAction.iconName = iconName
data.quickAction.tooltipText = I18n[description]
registerForUnbind(data.quickAction.onClicked(handler))
}
when (data.mod) {
is ModState.Installed -> if (data.mod.outdated) setupQuickAction("software-update-available-symbolic", "instance.settings.mods.update") {
data.mod.updates[0]()
}
else setupQuickAction("edit-delete-symbolic", "instance.settings.mods.delete") {
data.mod.remove()
}
is ModState.Available -> setupQuickAction("folder-download-symbolic", "instance.settings.mods.download") {
data.mod.install()
}
else -> setupQuickAction("dialog-question-symbolic", "instance.settings.mods.unknown") {
LauncherEnv.showError(I18n["instance.settings.mods.unknown.description"], I18n["instance.settings.mods.unknown"])
}
}
}
override fun UnbindContext.unbind(widget: ActionRow, data: Decomposed) {
// Not needed currently
}
override fun ActionContext.castWidget(widget: Widget): ActionRow = widget as ActionRow
override fun ActionContext.lookup(id: String, widget: ActionRow): Decomposed {
val mod = mods[id]
val suffixes = widget.firstChild!!.lastChild as Box
val quickAction = suffixes.firstChild as Button
return Decomposed(mod, quickAction)
}
}
data class Decomposed(val mod: ModState?, val quickAction: Button)
interface ModState {
val name: String
val summary: String
val description: String
class Installed(private val mod: Mod) : ModState {
private val sources = mod.metadata.sources
val updates: List<() -> Unit> = sources.mapNotNull { it.value.getOrNull() }.map { { mod.update(it) } }
val outdated get() = updates.isEmpty()
fun remove() = mod.delete()
override val name: String = mod.name
override val summary: String by lazy { mod.metadata.sources.bestSummary }
override val description: String by lazy { sources.bestDescription ?: mod.description.joinToString(separator = "\n") }
}
class Available(private val mod: ModSource, private val instance: Instance) : ModState {
fun install() {
//TODO thread and possibly show progress
ModManager.download(mod, instance.modsDir.resolve(mod.shortName + ModPath.EXT_IMOD), instance.mds)
.write()
}
override val name: String = mod.name
override val summary: String = mod.summary
override val description: String = mod.description
}
}
fun ModState(mod: ModSource): ModState {
val installed = mds.mods.filter { it.metadata.sources.keys.any { mod.projectMatches(it) } }
return if (installed.isEmpty()) ModState.Available(mod, instance)
else ModState.Installed(installed[0])
}
}

View File

@ -8,7 +8,7 @@ import io.gitlab.jfronny.inceptum.gtk.window.dialog.MicrosoftLoginDialog
import io.gitlab.jfronny.inceptum.launcher.api.account.AccountManager
import org.gnome.gtk.*
class AccountsTab(window: Window?) : SectionedSettingsTab(window) {
class AccountsTab(window: Window?) : SectionedSettingsTab<Window>(window) {
init {
section(null) {
build()

View File

@ -5,7 +5,7 @@ import io.gitlab.jfronny.inceptum.common.model.inceptum.UpdateChannel
import io.gitlab.jfronny.inceptum.gtk.control.settings.SectionedSettingsTab
import org.gnome.gtk.Window
class GeneralTab(window: Window?) : SectionedSettingsTab(window) {
class GeneralTab(window: Window?) : SectionedSettingsTab<Window>(window) {
init {
section(null) {
row("settings.general.snapshots", "settings.general.snapshots.subtitle") {

View File

@ -4,7 +4,8 @@ module io.gitlab.jfronny.inceptum.launcher.gtk {
requires kotlin.stdlib;
requires org.gnome.glib;
requires org.gnome.gtk;
requires org.gnome.adwaita;
requires org.gnome.adw;
requires org.gnome.pango;
// Should theoretically already be included transitively through inceptum.launcher and inceptum.common
requires io.gitlab.jfronny.commons;

View File

@ -59,8 +59,7 @@ instance.settings.general.args.client.subtitle=Arguments to add to Minecraft Cli
instance.settings.general.args.server.subtitle=Arguments to add to Minecraft Servers
instance.settings.export=Export
instance.settings.mods=Mods
instance.settings.mods.unsupported=Mod management is currently unavailable in the GTK UI.\
Please use the ImGUI or CLI to manage mods for now.
instance.settings.mods.select=Select a mod to view information about it
instance.settings.export.title=%1$s
instance.settings.export.subtitle=Export this Pack as a %1$s %2$s file
instance.settings.export.version=Version
@ -112,4 +111,9 @@ menu.file.new.file.error=Could not import Instance
menu.file.new.new=Create
instance.settings.mods.local=Local
instance.settings.mods.modrinth=Modrinth
instance.settings.mods.curseforge=CurseForge
instance.settings.mods.curseforge=CurseForge
instance.settings.mods.download=Download
instance.settings.mods.delete=Delete
instance.settings.mods.update=Update
instance.settings.mods.unknown=Unknown
instance.settings.mods.unknown.description=Unknown mod state

View File

@ -59,8 +59,7 @@ instance.settings.general.args.client.subtitle=Argumente f
instance.settings.general.args.server.subtitle=Argumente für Minecraft-Server
instance.settings.export=Export
instance.settings.mods=Mods
instance.settings.mods.unsupported=Die Verwaltung von Modifikationen ist momentan noch in Entwicklung.\
Bitte verwenden Sie vorerst die ImGUI oder CLI, um Mods zu verwalten.
instance.settings.mods.select=Wählen sie eine Mod um Informationen über sie zu sehen
instance.settings.export.title=%1$s
instance.settings.export.subtitle=Dieses Pack als %2$s für %1$s exportieren
instance.settings.export.version=Version
@ -112,4 +111,9 @@ menu.file.new.file.error=Konnte Instanz nicht importieren
menu.file.new.new=Erstellen
instance.settings.mods.local=Lokal
instance.settings.mods.modrinth=Modrinth
instance.settings.mods.curseforge=CurseForge
instance.settings.mods.curseforge=CurseForge
instance.settings.mods.download=Herunterladen
instance.settings.mods.delete=Löschen
instance.settings.mods.update=Aktualisieren
instance.settings.mods.unknown=Unbekannt
instance.settings.mods.unknown.description=Mod in unbekanntem Zustand

View File

@ -93,29 +93,13 @@ public class AddModWindow extends Window {
ImGui.text("Installed");
} else {
if (ImGui.button("Add##" + projectId)) {
ModrinthVersion stable = null;
ModrinthVersion beta = null;
ModrinthVersion latest = null;
for (ModrinthVersion version : ModrinthApi.getVersions(projectId)) {
if (version.game_versions().contains(instance.getGameVersion()) && version.loaders().contains("fabric")) {
latest = version;
if (version.version_type() == ModrinthVersion.VersionType.beta || version.version_type() == ModrinthVersion.VersionType.release) {
beta = version;
}
if (version.version_type() == ModrinthVersion.VersionType.release) {
stable = version;
}
}
}
if (stable != null) beta = stable;
if (beta != null) latest = beta;
ModrinthVersion latest = ModrinthApi.getLatestVersions(projectId, instance.getGameVersion()).latest();
if (latest == null) {
LauncherEnv.showError("No valid version could be identified for this mod", "No version found");
} else {
ModrinthVersion finalLatest = latest;
new Thread(() -> {
try {
ModManager.download(new ModrinthModSource(finalLatest.id()), instance.getModsDir().resolve((mod.slug() == null ? projectId : mod.slug()) + ModPath.EXT_IMOD), instance.mds()).write();
ModManager.download(new ModrinthModSource(latest.id()), instance.getModsDir().resolve((mod.slug() == null ? projectId : mod.slug()) + ModPath.EXT_IMOD), instance.mds()).write();
} catch (IOException e) {
LauncherEnv.showError("Could not download mod", e);
}

View File

@ -2,7 +2,6 @@ package io.gitlab.jfronny.inceptum.imgui.window;
import io.gitlab.jfronny.commons.io.JFiles;
import io.gitlab.jfronny.inceptum.common.MetaHolder;
import io.gitlab.jfronny.inceptum.common.Utils;
import io.gitlab.jfronny.inceptum.imgui.GuiMain;
import io.gitlab.jfronny.inceptum.imgui.window.dialog.ProcessStateWatcherWindow;
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv;
@ -44,17 +43,14 @@ public class GuiUtil {
GuiMain.open(new ProcessStateWatcherWindow("Creating Instance", "Could not create instance", pState, () -> {
for (Step step : Steps.STEPS) {
if (state.isCancelled()) {
try {
JFiles.deleteRecursive(MetaHolder.INSTANCE_DIR.resolve(state.name()));
} catch (IOException e) {
Utils.LOGGER.error("Could not delete instance dir", e);
}
state.tryRemoveInstance();
return;
}
pState.incrementStep(step.getName());
step.execute(state);
}
state.clearSetupLock();
LauncherEnv.showInfo("The instance was successfully created. You can now launch it using the main menu", "Successfully installed");
}));
}, t -> state.tryRemoveInstance()));
}
}

View File

@ -1,16 +1,22 @@
package io.gitlab.jfronny.inceptum.imgui.window.dialog;
import imgui.ImGui;
import io.gitlab.jfronny.commons.ref.R;
import io.gitlab.jfronny.commons.throwable.ThrowingRunnable;
import io.gitlab.jfronny.inceptum.imgui.window.Window;
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv;
import io.gitlab.jfronny.inceptum.launcher.util.ProcessState;
import java.util.function.Consumer;
public class ProcessStateWatcherWindow extends Window {
private final ProcessState state;
private boolean finished;
public ProcessStateWatcherWindow(String title, String errorMessage, ProcessState state, ThrowingRunnable<?> executor) {
this(title, errorMessage, state, executor, R::nop);
}
public ProcessStateWatcherWindow(String title, String errorMessage, ProcessState state, ThrowingRunnable<?> executor, Consumer<Throwable> onFail) {
super(title);
this.state = state;
new Thread(() -> {
@ -18,6 +24,7 @@ public class ProcessStateWatcherWindow extends Window {
executor.run();
} catch (Throwable e) {
state.cancel();
onFail.accept(e);
LauncherEnv.showError(errorMessage, e);
} finally {
finished = true;

View File

@ -57,6 +57,10 @@ public class CurseforgeApi {
return checkDistribution(Net.downloadObject(API_URL + "mods/" + id, GC_GetModResponse::read, API_KEY).data());
}
public static String getDescription(int id) throws IOException {
return Net.downloadObject(API_URL + "mods/" + id + "/description", GC_GetModDescriptionResponse::read, API_KEY).data();
}
private static CurseforgeMod checkDistribution(CurseforgeMod mod) {
if (!mod.allowModDistribution()) {
throw new IllegalArgumentException("The author of the mod \"" + mod.slug() + "\" has chosen to deliberately break your ability of downloading it.\n"

View File

@ -4,6 +4,7 @@ import io.gitlab.jfronny.gson.compile.util.GList;
import io.gitlab.jfronny.gson.stream.JsonReader;
import io.gitlab.jfronny.inceptum.common.Net;
import io.gitlab.jfronny.inceptum.launcher.model.modrinth.*;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.StringReader;
@ -43,6 +44,22 @@ public class ModrinthApi {
return Net.downloadObject(API_HOST + "v2/version/" + id, GC_ModrinthVersion::read);
}
public static ModrinthLatest getLatestVersions(String mod, String gameVersion) throws IOException {
ModrinthVersion stable = null;
ModrinthVersion beta = null;
ModrinthVersion latest = null;
for (ModrinthVersion version : ModrinthApi.getVersions(mod)) {
if (version.game_versions().contains(gameVersion) && version.loaders().contains("fabric")) {
latest = version;
if (version.version_type() == ModrinthVersion.VersionType.beta || version.version_type() == ModrinthVersion.VersionType.release)
beta = version;
if (version.version_type() == ModrinthVersion.VersionType.release)
stable = version;
}
}
return new ModrinthLatest(stable, beta, latest);
}
public static ModrinthVersion getVersionByHash(String sha1) throws IOException {
return Net.downloadObject(API_HOST + "v2/version_file/" + sha1 + "?algorithm=sha1", GC_ModrinthVersion::read);
}

View File

@ -0,0 +1,8 @@
package io.gitlab.jfronny.inceptum.launcher.model.curseforge.response;
import io.gitlab.jfronny.gson.compile.annotations.GSerializable;
import io.gitlab.jfronny.inceptum.common.GsonPreset;
@GSerializable(configure = GsonPreset.Api.class)
public record GetModDescriptionResponse(String data) {
}

View File

@ -3,6 +3,7 @@ package io.gitlab.jfronny.inceptum.launcher.model.inceptum;
import io.gitlab.jfronny.commons.HashUtils;
import io.gitlab.jfronny.commons.data.MutCollection;
import io.gitlab.jfronny.commons.data.delegate.DelegateMap;
import io.gitlab.jfronny.commons.serialize.gson.api.v1.Ignore;
import io.gitlab.jfronny.gson.compile.annotations.GPrefer;
import io.gitlab.jfronny.gson.compile.annotations.GSerializable;
import io.gitlab.jfronny.inceptum.common.GsonPreset;
@ -11,7 +12,9 @@ import io.gitlab.jfronny.inceptum.launcher.api.CurseforgeApi;
import io.gitlab.jfronny.inceptum.launcher.api.ModrinthApi;
import io.gitlab.jfronny.inceptum.launcher.gson.ModMetaSourcesAdapter;
import io.gitlab.jfronny.inceptum.launcher.model.curseforge.response.FingerprintMatchesResponse;
import io.gitlab.jfronny.inceptum.launcher.system.instance.Mod;
import io.gitlab.jfronny.inceptum.launcher.system.source.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
@ -34,6 +37,29 @@ public record ModMeta(
public Sources() {
super(MutCollection.mapOf());
}
private Optional<ModSource> getPreferredMetadataSource() {
return keySet().stream().max((left, right) -> {
if (left.equals(right)) return 0;
if (left instanceof ModrinthModSource) return 1;
if (right instanceof ModrinthModSource) return -1;
if (left instanceof CurseforgeModSource) return 1;
if (right instanceof CurseforgeModSource) return -1;
return 1;
});
}
public @NotNull String getBestSummary() {
return getPreferredMetadataSource()
.map(ModSource::getSummary)
.orElse("Local Mod");
}
public @Nullable String getBestDescription() {
return getPreferredMetadataSource()
.map(ModSource::getDescription)
.orElse(null);
}
}
@GPrefer

View File

@ -0,0 +1,20 @@
package io.gitlab.jfronny.inceptum.launcher.model.modrinth;
import org.jetbrains.annotations.Nullable;
public record ModrinthLatest(@Nullable ModrinthVersion stable, @Nullable ModrinthVersion beta, @Nullable ModrinthVersion latest) {
public @Nullable ModrinthVersion get(ModrinthVersion.VersionType type) {
return switch (type) {
case alpha -> latest;
case beta -> beta;
case release -> stable;
};
}
public @Nullable ModrinthVersion getBest() {
if (stable != null) return stable;
if (beta != null) return beta;
if (latest != null) return latest;
return null;
}
}

View File

@ -1,9 +1,16 @@
package io.gitlab.jfronny.inceptum.launcher.system.setup;
import io.gitlab.jfronny.commons.io.JFiles;
import io.gitlab.jfronny.inceptum.common.MetaHolder;
import io.gitlab.jfronny.inceptum.common.Utils;
import io.gitlab.jfronny.inceptum.launcher.model.mojang.VersionInfo;
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance;
import io.gitlab.jfronny.inceptum.launcher.system.instance.LoaderInfo;
import io.gitlab.jfronny.inceptum.launcher.util.ProcessState;
import java.io.IOException;
import java.nio.file.Files;
public record SetupStepInfo(VersionInfo version,
LoaderInfo loader,
String name,
@ -15,4 +22,23 @@ public record SetupStepInfo(VersionInfo version,
public boolean isCancelled() {
return currentState.isCancelled();
}
public void tryRemoveInstance() {
try {
removeInstance();
} catch (IOException e) {
Utils.LOGGER.error("Could not delete instance dir", e);
}
}
public void removeInstance() throws IOException {
JFiles.deleteRecursive(MetaHolder.INSTANCE_DIR.resolve(name));
}
public void clearSetupLock() {
try {
Files.deleteIfExists(MetaHolder.INSTANCE_DIR.resolve(name).resolve(Instance.SETUP_LOCK_NAME));
} catch (IOException ignored) {
}
}
}

View File

@ -15,11 +15,11 @@ import java.util.*;
public class Steps {
public static Set<Step> STEPS = new LinkedHashSet<>(List.of(
new SetupDirsStep(),
new WriteMetadataStep(),
new DownloadJavaStep(),
new DownloadClientStep(),
new DownloadLibrariesStep(),
new DownloadAssetsStep(),
new WriteMetadataStep(),
new RunMdsStep()
));
@ -29,24 +29,28 @@ public class Steps {
public static void reDownload(Instance instance, ProcessState state) throws IOException {
if (instance.isLocked()) return;
boolean found = false;
for (VersionsListInfo version : McApi.getVersions().versions()) {
if (version.id.equals(instance.getGameVersion())) {
found = true;
VersionInfo vi = McApi.getVersionInfo(version);
if (instance.isFabric())
vi = FabricMetaApi.addFabric(vi, instance.getLoaderVersion(), FabricMetaApi.FabricVersionInfoType.Both);
LoaderInfo li = instance.isFabric()
? new LoaderInfo(LoaderInfo.Type.Fabric, instance.getLoaderVersion())
: LoaderInfo.NONE;
SetupStepInfo info = new SetupStepInfo(vi, li, instance.getName(), state);
for (Step step : Steps.STEPS) {
state.incrementStep(step.getName());
step.execute(info);
if (state.isCancelled()) return;
try {
boolean found = false;
for (VersionsListInfo version : McApi.getVersions().versions()) {
if (version.id.equals(instance.getGameVersion())) {
found = true;
VersionInfo vi = McApi.getVersionInfo(version);
if (instance.isFabric())
vi = FabricMetaApi.addFabric(vi, instance.getLoaderVersion(), FabricMetaApi.FabricVersionInfoType.Both);
LoaderInfo li = instance.isFabric()
? new LoaderInfo(LoaderInfo.Type.Fabric, instance.getLoaderVersion())
: LoaderInfo.NONE;
SetupStepInfo info = new SetupStepInfo(vi, li, instance.getName(), state);
for (Step step : Steps.STEPS) {
state.incrementStep(step.getName());
step.execute(info);
if (state.isCancelled()) return;
}
}
}
if (!found) throw new IOException("Could not identify minecraft version " + instance.getGameVersion());
} finally {
instance.setSetupLock(false);
}
if (!found) throw new IOException("Could not identify minecraft version " + instance.getGameVersion());
}
}

View File

@ -22,7 +22,6 @@ public class WriteMetadataStep implements Step {
meta.gameVersion = info.version().id;
GC_InstanceMeta.write(meta, metaPath);
}
Instance.setSetupLock(instance, false);
if (!Files.exists(instance.resolve(".gitignore"))) {
Files.writeString(instance.resolve(".gitignore"), """
realms_persistence.json

View File

@ -1,6 +1,7 @@
package io.gitlab.jfronny.inceptum.launcher.system.source;
import io.gitlab.jfronny.commons.HashUtils;
import io.gitlab.jfronny.commons.StringFormatter;
import io.gitlab.jfronny.commons.cache.MemoryOperationResultCache;
import io.gitlab.jfronny.commons.tuple.Triple;
import io.gitlab.jfronny.commons.tuple.Tuple;
@ -102,11 +103,35 @@ public final class CurseforgeModSource implements ModSource {
return current.fileName();
}
@Override
public String getDescription() {
try {
return CurseforgeApi.getDescription(mod.id());
} catch (IOException e) {
return "Could not get description\n\n" + StringFormatter.toString(e);
}
}
@Override
public String getSummary() {
return mod.summary();
}
@Override
public boolean equals(Object obj) {
return obj instanceof ModSource ms && equals(ms);
}
@Override
public boolean equals(ModSource other) {
return other instanceof CurseforgeModSource cu && cu.projectId == projectId && cu.fileId == fileId;
}
@Override
public boolean projectMatches(ModSource other) {
return other instanceof CurseforgeModSource cu && cu.projectId == projectId;
}
public int getFileId() {
return fileId;
}

View File

@ -57,8 +57,31 @@ public record DirectModSource(String fileName, String url, Set<ModSource> depend
return fileName;
}
@Override
public String getDescription() {
return "Downloaded directly, no description is available";
}
@Override
public String getSummary() {
return "Downloaded directly, no description is available";
}
@Override
public boolean equals(Object obj) {
return obj instanceof ModSource ms && equals(ms);
}
@Override
public boolean equals(ModSource other) {
return false;
return other instanceof DirectModSource dm
&& dm.url.equals(url)
&& dm.fileName.equals(fileName)
&& dm.dependencies.equals(dependencies);
}
@Override
public boolean projectMatches(ModSource other) {
return other instanceof DirectModSource dm && dm.url.equals(url);
}
}

View File

@ -26,8 +26,14 @@ public interface ModSource {
String getFileName();
String getDescription();
String getSummary();
boolean equals(ModSource other);
boolean projectMatches(ModSource other);
default Path getJarPath() {
return MetaHolder.LIBRARIES_DIR.resolve("com").resolve(getName()).resolve(getFileName());
}

View File

@ -56,23 +56,7 @@ public final class ModrinthModSource implements ModSource {
@Override
public Optional<ModSource> getUpdate(String gameVersion) throws IOException {
return UPDATE_CACHE.get(Tuple.of(versionId, gameVersion), () -> {
ModrinthVersion stable = null;
ModrinthVersion beta = null;
ModrinthVersion latest = null;
for (ModrinthVersion version : ModrinthApi.getVersions(getModId())) {
if (version.game_versions().contains(gameVersion) && version.loaders().contains("fabric")) {
latest = version;
if (version.version_type() == ModrinthVersion.VersionType.beta || version.version_type() == ModrinthVersion.VersionType.release)
beta = version;
if (version.version_type() == ModrinthVersion.VersionType.release)
stable = version;
}
}
ModrinthVersion next = switch (current.version_type()) {
case alpha -> latest;
case beta -> beta;
case release -> stable;
};
ModrinthVersion next = ModrinthApi.getLatestVersions(getModId(), gameVersion).get(current.version_type());
if (next == null) return Optional.empty();
if (next.version_number().equals(current.version_number())) return Optional.empty();
return Optional.of(new ModrinthModSource(next.id()));
@ -99,11 +83,31 @@ public final class ModrinthModSource implements ModSource {
return current.files().get(0).filename();
}
@Override
public String getDescription() {
return mod.body();
}
@Override
public String getSummary() {
return mod.description();
}
@Override
public boolean equals(Object obj) {
return obj instanceof ModSource ms && equals(ms);
}
@Override
public boolean equals(ModSource other) {
return other instanceof ModrinthModSource ms && ms.getModId().equals(getModId()) && ms.versionId.equals(versionId);
}
@Override
public boolean projectMatches(ModSource other) {
return other instanceof ModrinthModSource ms && ms.getModId().equals(getModId());
}
public String getVersionId() {
return versionId;
}