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.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 } init { instance = window.instance mds = instance.mds content.apply { //TODO consider filter panel via Flap homogeneous = false canNavigateBack = true transitionType = LeafletTransitionType.OVER append(Box(Orientation.VERTICAL, 6).apply { content.visibleChild = this append(KDropDown("instance.settings.mods.local", "instance.settings.mods.modrinth", "instance.settings.mods.curseforge").apply { onChange { newFilter -> when (newFilter) { 0 -> switchTo(Page.LOCAL) 1 -> switchTo(Page.MODRINTH) 2 -> switchTo(Page.CURSEFORGE) } } }) append(SearchBar().apply { hexpand = false showCloseButton = false searchMode = false val entry = SearchEntry().apply { onSearchChanged { updateSearch(text) } } 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 { 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 } } } 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]) } }