diff --git a/core/src/com/unciv/ui/screens/modmanager/ModManagementScreen.kt b/core/src/com/unciv/ui/screens/modmanager/ModManagementScreen.kt index 375cad6ce8..9e5ee05f65 100644 --- a/core/src/com/unciv/ui/screens/modmanager/ModManagementScreen.kt +++ b/core/src/com/unciv/ui/screens/modmanager/ModManagementScreen.kt @@ -4,6 +4,7 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Touchable +import com.badlogic.gdx.scenes.scene2d.ui.Cell import com.badlogic.gdx.scenes.scene2d.ui.Label import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane import com.badlogic.gdx.scenes.scene2d.ui.Table @@ -14,6 +15,8 @@ import com.unciv.UncivGame import com.unciv.json.fromJsonFile import com.unciv.json.json import com.unciv.logic.UncivShowableException +import com.unciv.logic.github.Github +import com.unciv.logic.github.Github.repoNameToFolderName import com.unciv.logic.github.GithubAPI import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache @@ -34,6 +37,7 @@ import com.unciv.ui.components.input.onActivation import com.unciv.ui.components.input.onClick import com.unciv.ui.components.widgets.AutoScrollPane import com.unciv.ui.components.widgets.ExpanderTab +import com.unciv.ui.components.widgets.LoadingImage import com.unciv.ui.components.widgets.WrappableLabel import com.unciv.ui.images.ImageGetter import com.unciv.ui.popups.ConfirmPopup @@ -43,8 +47,6 @@ import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.basescreen.RecreateOnResize import com.unciv.ui.screens.mainmenuscreen.MainMenuScreen import com.unciv.ui.screens.modmanager.ModManagementOptions.SortType -import com.unciv.logic.github.Github -import com.unciv.logic.github.Github.repoNameToFolderName import com.unciv.ui.screens.pickerscreens.PickerScreen import com.unciv.utils.Concurrency import com.unciv.utils.Log @@ -81,6 +83,14 @@ class ModManagementScreen private constructor( } } + // Since we're `RecreateOnResize`, preserve the portrait/landscape mode for our lifetime + private val isPortrait: Boolean + + // Will hold a LoadingImage until the online query is done, then it is freed/nulled + private var loading: LoadingImage? = null + // Holds the Cell in portrait mode which initially gets the loading image and later the options widget + private var optionsCell: Cell? = null + // Left column (in landscape, portrait stacks them within expanders) private val installedModsTable = Table().apply { defaults().pad(10f) } private val scrollInstalledMods = AutoScrollPane(installedModsTable) @@ -90,7 +100,7 @@ class ModManagementScreen private constructor( // Right column private val modActionTable = ModInfoAndActionPane() private val scrollActionTable = AutoScrollPane(modActionTable) - // Factory for the Widget floating top right + // Manager providing the Widget floating top right in landscape mode, stacked expander in portrait private val optionsManager = ModManagementOptions(this) private var lastSelectedButton: ModDecoratedButton? = null @@ -112,6 +122,8 @@ class ModManagementScreen private constructor( // cleanup - background processing needs to be stopped on exit and memory freed private var runningSearchJob: Job? = null + // This is only set for cleanup, not when the user stops the query (by clicking the loading icon) + // Therefore, finding `runningSearchJob?.isActive == false && !stopBackgroundTasks` means stopped by user private var stopBackgroundTasks = false override fun dispose() { @@ -151,8 +163,10 @@ class ModManagementScreen private constructor( labelWrapper.add(modDescriptionLabel).row() labelScroll.actor = labelWrapper - if (isNarrowerThan4to3()) initPortrait() + isPortrait = isNarrowerThan4to3() + if (isPortrait) initPortrait() else initLandscape() + showLoadingImage() if (installedModInfo.isEmpty()) refreshInstalledModInfo() @@ -168,7 +182,8 @@ class ModManagementScreen private constructor( private fun initPortrait() { topTable.defaults().top().pad(0f) - topTable.add(optionsManager.expander).top().growX().row() + optionsCell = topTable.add().top().growX() + topTable.row() installedExpanderTab = ExpanderTab(optionsManager.getInstalledHeader(), expanderWidth = stage.width) { it.add(scrollInstalledMods).growX().maxHeight(stage.height / 2) @@ -213,19 +228,57 @@ class ModManagementScreen private constructor( topTable.add(scrollActionTable) topTable.add().expandX().row() - stage.addActor(optionsManager.expander) - optionsManager.expanderChangeEvent = { - optionsManager.expander.pack() - optionsManager.expander.setPosition(stage.width - 2f, stage.height - 2f, Align.topRight) + } + + private fun showLoadingImage() { + val loadingStyle = LoadingImage.Style(circleColor = Color.DARK_GRAY, loadingColor = Color.FIREBRICK) + // Size should fit where the Search and Filter expander will go, which is 48f high and set topRight with 2f margin. + // When changing this, please also change the setPosition in landscape mode + val loading = LoadingImage(40f, loadingStyle) + this.loading = loading + + if (isPortrait) { + optionsCell!!.pad(4f) + optionsCell!!.setActor(loading) + } else {// mute complaints - we _know_ it's not null + optionsManager.expander.remove() + loading.setPosition(stage.width - 6f, stage.height - 6f, Align.topRight) + stage.addActor(loading) + } + + loading.show() // Now that it's on stage, start animation + // Allow clicking the loading icon to stop the query + loading.onClick { + if (runningSearchJob?.isActive != true) return@onClick + runningSearchJob?.cancel() + markOnlineQueryIncomplete() + replaceLoadingWithOptions() + } + } + + private fun replaceLoadingWithOptions() { + val actorToRemove = loading ?: return + loading = null + actorToRemove.remove() // This is able to remove from a Cell or (the floating version) from the parent's children + actorToRemove.dispose() + + if (isPortrait) { + optionsCell!!.pad(0f) + optionsCell!!.setActor(optionsManager.expander) + } else { + stage.addActor(optionsManager.expander) + optionsManager.expanderChangeEvent = { + optionsManager.expander.pack() + optionsManager.expander.setPosition(stage.width - 2f, stage.height - 2f, Align.topRight) + } + optionsManager.expanderChangeEvent?.invoke() } - optionsManager.expanderChangeEvent?.invoke() } private fun reloadOnlineMods() { onlineModsTable.clear() onlineModInfo.clear() onlineModsTable.add(getDownloadFromUrlButton()).padBottom(15f).row() - onlineModsTable.add("...".toLabel()).row() tryDownloadPage(1) } @@ -242,6 +295,7 @@ class ModManagementScreen private constructor( Log.error("Could not download mod list", ex) launchOnGLThread { ToastPopup("Could not download mod list", this@ModManagementScreen) + replaceLoadingWithOptions() } runningSearchJob = null return@run @@ -257,13 +311,6 @@ class ModManagementScreen private constructor( } private fun addModInfoFromRepoSearch(repoSearch: GithubAPI.RepoSearch, pageNum: Int) { - // clear and remove last cell if it is the "..." indicator - val lastCell = onlineModsTable.cells.lastOrNull() - if (lastCell != null && lastCell.actor is Label && (lastCell.actor as Label).text.toString() == "...") { - lastCell.setActor(null) - onlineModsTable.cells.removeValue(lastCell, true) - } - for (repo in repoSearch.items) { if (stopBackgroundTasks) return repo.name = repo.name.repoNameToFolderName() @@ -315,15 +362,8 @@ class ModManagementScreen private constructor( if (repoSearch.items.size < amountPerPage) { // Check: It is also not impossible we missed a mod - just inform user if (repoSearch.incomplete_results) { - val retryLabel = "Online query result is incomplete".toLabel(Color.RED) - retryLabel.touchable = Touchable.enabled - retryLabel.onClick { reloadOnlineMods() } - onlineModsTable.add(retryLabel) + markOnlineQueryIncomplete() } - } else { - // the page was full so there may be more pages. - // indicate that search will be continued - onlineModsTable.add("...".toLabel()).row() } onlineModsTable.pack() @@ -334,6 +374,18 @@ class ModManagementScreen private constructor( // continue search unless last page was reached if (repoSearch.items.size >= amountPerPage && !stopBackgroundTasks) tryDownloadPage(pageNum + 1) + else + replaceLoadingWithOptions() + } + + private fun markOnlineQueryIncomplete() { + val retryLabel = "Online query result is incomplete".toLabel(Color.RED) + retryLabel.touchable = Touchable.enabled + retryLabel.onClick { + showLoadingImage() + reloadOnlineMods() + } + onlineModsTable.add(retryLabel) } private fun syncOnlineSelected(modName: String, button: ModDecoratedButton) {