281 lines
12 KiB
Kotlin
281 lines
12 KiB
Kotlin
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<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
|
|
}
|
|
|
|
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<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
|
|
}
|
|
}
|
|
}
|
|
|
|
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])
|
|
}
|
|
}
|