diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 8e8b9f4..e239ad1 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -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") } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/inceptum.java.gradle.kts b/buildSrc/src/main/kotlin/inceptum.java.gradle.kts index c97b26f..af4cac0 100644 --- a/buildSrc/src/main/kotlin/inceptum.java.gradle.kts +++ b/buildSrc/src/main/kotlin/inceptum.java.gradle.kts @@ -1,5 +1,5 @@ plugins { - `java-library` + id("jf.java") `maven-publish` } diff --git a/common/src/main/java/io/gitlab/jfronny/inceptum/common/Net.java b/common/src/main/java/io/gitlab/jfronny/inceptum/common/Net.java index d5e3272..2cda26d 100644 --- a/common/src/main/java/io/gitlab/jfronny/inceptum/common/Net.java +++ b/common/src/main/java/io/gitlab/jfronny/inceptum/common/Net.java @@ -39,7 +39,7 @@ public class Net { } public static T downloadObject(String url, ThrowingFunction 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 downloadObject(String url, String sha1, ThrowingFunction 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)); diff --git a/launcher-gtk/build.gradle.kts b/launcher-gtk/build.gradle.kts index e3158ce..5c20bc7 100644 --- a/launcher-gtk/build.gradle.kts +++ b/launcher-gtk/build.gradle.kts @@ -3,13 +3,18 @@ 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") { diff --git a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/InstanceListEntryFactory.kt b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/InstanceListEntryFactory.kt index 581a07f..612d7b1 100644 --- a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/InstanceListEntryFactory.kt +++ b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/InstanceListEntryFactory.kt @@ -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 diff --git a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/LoadingRevealer.kt b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/LoadingRevealer.kt new file mode 100644 index 0000000..114f4d8 --- /dev/null +++ b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/LoadingRevealer.kt @@ -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() + } +} \ No newline at end of file diff --git a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/settings/SectionedSettingsTab.kt b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/settings/SectionedSettingsTab.kt index 56c14de..f627258 100644 --- a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/settings/SectionedSettingsTab.kt +++ b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/settings/SectionedSettingsTab.kt @@ -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(window, Box(Orientation.VERTICAL, 8)) { +open class SectionedSettingsTab(window: W?): SettingsTab(window, Box(Orientation.VERTICAL, 8)) { init { content.marginHorizontal = 24 content.marginTop = 12 diff --git a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/settings/SettingsTab.kt b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/settings/SettingsTab.kt index 1081c6b..581364d 100644 --- a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/settings/SettingsTab.kt +++ b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/settings/SettingsTab.kt @@ -5,8 +5,8 @@ import io.gitlab.jfronny.inceptum.gtk.GtkEnvBackend import org.gnome.gtk.Widget import org.gnome.gtk.Window -open class SettingsTab( - protected val window: Window?, +open class SettingsTab( + protected val window: W?, val content: T ) { protected fun showError(message: String, t: Throwable) = diff --git a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/settings/SettingsWindow.kt b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/settings/SettingsWindow.kt index 959ae5e..3d048f3 100644 --- a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/settings/SettingsWindow.kt +++ b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/settings/SettingsWindow.kt @@ -40,7 +40,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) } diff --git a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/util/StringListExt.kt b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/util/StringListExt.kt index 6d6c618..047137b 100644 --- a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/util/StringListExt.kt +++ b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/util/StringListExt.kt @@ -5,5 +5,6 @@ import org.gnome.gtk.StringList fun StringList.clear() = splice(0, size, null) fun StringList.addAll(values: Array) = splice(size, 0, values) +fun StringList.replaceAll(values: Array) = splice(0, size, values) val ListModel.size get() = nItems \ No newline at end of file diff --git a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/instance/ExportTab.kt b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/instance/ExportTab.kt index 76fe9ce..72865e5 100644 --- a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/instance/ExportTab.kt +++ b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/instance/ExportTab.kt @@ -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(window) { + private val instance: Instance = window.instance init { section(null) { row("instance.settings.export.version", "instance.settings.export.version.subtitle") { diff --git a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/instance/GeneralTab.kt b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/instance/GeneralTab.kt index 7d55e22..75ee020 100644 --- a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/instance/GeneralTab.kt +++ b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/instance/GeneralTab.kt @@ -14,7 +14,6 @@ import io.gitlab.jfronny.inceptum.gtk.util.markup import io.gitlab.jfronny.inceptum.gtk.util.toTypedArray 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 +25,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(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"]) diff --git a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/instance/InstanceSettingsWindow.kt b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/instance/InstanceSettingsWindow.kt index b2a00d8..2ca22c6 100644 --- a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/instance/InstanceSettingsWindow.kt +++ b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/instance/InstanceSettingsWindow.kt @@ -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") } } diff --git a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/instance/ModsTab.kt b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/instance/ModsTab.kt index 7a0a3cb..948365e 100644 --- a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/instance/ModsTab.kt +++ b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/instance/ModsTab.kt @@ -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(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(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,206 @@ 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() + 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? = null + //TODO search pagination + private val search = AsyncRequest({ + searchResult = null + loadingRevealer.setRunning(true) + loadingRevealer.pulse() + VoidFuture(ForkJoinPool.commonPool().submit { + val sources: List = 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() + + //TODO https://gitlab.gnome.org/World/gfeeds/-/blob/master/data/ui/sidebar_listbox_row.blp + inner class ModsListItemFactory: KSignalListItemFactory() { + 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.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]) + } } diff --git a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/launcher/AccountsTab.kt b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/launcher/AccountsTab.kt index b0ba746..f07253f 100644 --- a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/launcher/AccountsTab.kt +++ b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/launcher/AccountsTab.kt @@ -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) { init { section(null) { build() diff --git a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/launcher/GeneralTab.kt b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/launcher/GeneralTab.kt index 4286d11..4ff1dbc 100644 --- a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/launcher/GeneralTab.kt +++ b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/settings/launcher/GeneralTab.kt @@ -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) { init { section(null) { row("settings.general.snapshots", "settings.general.snapshots.subtitle") { diff --git a/launcher-gtk/src/main/resources/inceptum.properties b/launcher-gtk/src/main/resources/inceptum.properties index 4ade455..0713167 100644 --- a/launcher-gtk/src/main/resources/inceptum.properties +++ b/launcher-gtk/src/main/resources/inceptum.properties @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/launcher-gtk/src/main/resources/inceptum_de.properties b/launcher-gtk/src/main/resources/inceptum_de.properties index 357e474..a2982f7 100644 --- a/launcher-gtk/src/main/resources/inceptum_de.properties +++ b/launcher-gtk/src/main/resources/inceptum_de.properties @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/launcher-imgui/src/main/java/io/gitlab/jfronny/inceptum/imgui/window/AddModWindow.java b/launcher-imgui/src/main/java/io/gitlab/jfronny/inceptum/imgui/window/AddModWindow.java index 7299f75..6f334a3 100644 --- a/launcher-imgui/src/main/java/io/gitlab/jfronny/inceptum/imgui/window/AddModWindow.java +++ b/launcher-imgui/src/main/java/io/gitlab/jfronny/inceptum/imgui/window/AddModWindow.java @@ -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.getLatestVersion(projectId, instance.getGameVersion()); 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); } diff --git a/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/api/CurseforgeApi.java b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/api/CurseforgeApi.java index 43b03b3..004eea6 100644 --- a/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/api/CurseforgeApi.java +++ b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/api/CurseforgeApi.java @@ -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" diff --git a/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/api/ModrinthApi.java b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/api/ModrinthApi.java index 1814bb4..7cb556e 100644 --- a/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/api/ModrinthApi.java +++ b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/api/ModrinthApi.java @@ -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); } diff --git a/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/model/curseforge/response/GetModDescriptionResponse.java b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/model/curseforge/response/GetModDescriptionResponse.java new file mode 100644 index 0000000..cc66ffc --- /dev/null +++ b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/model/curseforge/response/GetModDescriptionResponse.java @@ -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) { +} diff --git a/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/model/inceptum/ModMeta.java b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/model/inceptum/ModMeta.java index 7c34fd6..384484a 100644 --- a/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/model/inceptum/ModMeta.java +++ b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/model/inceptum/ModMeta.java @@ -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 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 diff --git a/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/model/modrinth/ModrinthLatest.java b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/model/modrinth/ModrinthLatest.java new file mode 100644 index 0000000..a937664 --- /dev/null +++ b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/model/modrinth/ModrinthLatest.java @@ -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; + } +} diff --git a/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/source/CurseforgeModSource.java b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/source/CurseforgeModSource.java index 38b71e3..2a42fef 100644 --- a/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/source/CurseforgeModSource.java +++ b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/source/CurseforgeModSource.java @@ -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; } diff --git a/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/source/DirectModSource.java b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/source/DirectModSource.java index ead6ccd..ca7fff9 100644 --- a/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/source/DirectModSource.java +++ b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/source/DirectModSource.java @@ -57,8 +57,31 @@ public record DirectModSource(String fileName, String url, Set 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); } } diff --git a/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/source/ModSource.java b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/source/ModSource.java index 45b820b..0b6ddf4 100644 --- a/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/source/ModSource.java +++ b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/source/ModSource.java @@ -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()); } diff --git a/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/source/ModrinthModSource.java b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/source/ModrinthModSource.java index 7cba70e..7952761 100644 --- a/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/source/ModrinthModSource.java +++ b/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/source/ModrinthModSource.java @@ -56,23 +56,7 @@ public final class ModrinthModSource implements ModSource { @Override public Optional 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; }