From 729e64190183c871def270b7be74f9db2dad5cc9 Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Sat, 28 Aug 2021 12:06:48 +0200 Subject: [PATCH 01/25] Rewriting the mod in kotlin for better stability and performance Signed-off-by: DeathsGun --- build.gradle | 43 ++- gradle.properties | 4 + .../xyz/deathsgun/modmanager/ModManager.java | 96 ------- .../api/manipulation/ManipulationTask.java | 90 ------- .../api/manipulation/NetworkTask.java | 40 --- .../modmanager/gui/ModDetailScreen.java | 185 ------------- .../modmanager/gui/ModManagerErrorScreen.java | 78 ------ .../modmanager/gui/ModsOverviewScreen.java | 153 ----------- .../gui/widget/CategoryListEntry.java | 67 ----- .../gui/widget/CategoryListWidget.java | 88 ------- .../gui/widget/DescriptionListEntry.java | 75 ------ .../gui/widget/DescriptionWidget.java | 79 ------ .../modmanager/gui/widget/ModListEntry.java | 131 ---------- .../modmanager/gui/widget/ModListWidget.java | 153 ----------- .../ModManagerTexturedButtonWidget.java | 28 -- .../gui/widget/better/BetterListWidget.java | 215 --------------- .../modmanager/manager/IconManager.java | 76 ------ .../manager/ModManipulationManager.java | 75 ------ .../modmanager/mixin/ModsScreenMixin.java | 54 ---- .../modmanager/mixin/TitleScreenMixin.java | 56 ---- .../modmanager/model/ReducedModMetadata.java | 15 -- .../providers/modrinth/Modrinth.java | 155 ----------- .../providers/modrinth/model/Mod.java | 52 ---- .../providers/modrinth/model/ModResult.java | 61 ----- .../modrinth/model/SearchResponse.java | 42 --- .../providers/modrinth/model/Version.java | 67 ----- .../services/ManipulationService.java | 69 ----- .../services/UpdateCheckService.java | 164 ------------ .../modmanager/tasks/IconDownloadTask.java | 64 ----- .../modmanager/tasks/ModDownloadTask.java | 44 ---- .../modmanager/tasks/ModRemovalTask.java | 60 ----- .../modmanager/tasks/ModUpdateTask.java | 48 ---- .../deathsgun/modmanager/util/FabricMods.java | 64 ----- .../modmanager/util/InstallationUtil.java | 61 ----- .../modmanager/util/MarkdownPreprocessor.java | 79 ------ .../xyz/deathsgun/modmanager/Initializer.kt} | 15 +- .../xyz/deathsgun/modmanager/ModManager.kt} | 16 +- .../modmanager/api/http/CategoriesResult.kt} | 13 +- .../modmanager/api/http/ModResult.kt} | 16 +- .../modmanager/api/http/ModsResult.kt | 25 ++ .../modmanager/api/http/VersionResult.kt} | 14 +- .../xyz/deathsgun/modmanager/api/mod/Asset.kt | 23 ++ .../deathsgun/modmanager/api/mod/Category.kt | 25 ++ .../xyz/deathsgun/modmanager/api/mod/Mod.kt} | 25 +- .../deathsgun/modmanager/api/mod/State.kt} | 13 +- .../deathsgun/modmanager/api/mod/Version.kt | 25 ++ .../modmanager/api/mod/VersionType.kt} | 10 +- .../modmanager/api/provider/IModProvider.kt} | 52 ++-- .../api/provider/IModUpdateProvider.kt | 25 ++ .../modmanager/api/provider/Sorting.kt} | 11 +- .../xyz/deathsgun/modmanager/config/Config.kt | 24 ++ .../modmanager/providers/modrinth/Modrinth.kt | 245 ++++++++++++++++++ .../providers/modrinth/models/DetailedMod.kt} | 26 +- .../providers/modrinth/models/ModResult.kt | 33 +++ .../modrinth/models/ModrinthVersion.kt} | 40 +-- .../providers/modrinth/models/SearchResult.kt | 56 ++++ src/main/resources/fabric.mod.json | 16 +- .../providers/modrinth/ModrinthTest.kt | 169 ++++++++++++ 58 files changed, 819 insertions(+), 2929 deletions(-) delete mode 100644 src/main/java/xyz/deathsgun/modmanager/ModManager.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/api/manipulation/ManipulationTask.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/api/manipulation/NetworkTask.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/gui/ModDetailScreen.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/gui/ModManagerErrorScreen.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/gui/widget/CategoryListEntry.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/gui/widget/DescriptionListEntry.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/gui/widget/ModListEntry.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/gui/widget/ModListWidget.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/gui/widget/ModManagerTexturedButtonWidget.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/gui/widget/better/BetterListWidget.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/manager/IconManager.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/manager/ModManipulationManager.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/model/ReducedModMetadata.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/Mod.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/ModResult.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/SearchResponse.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/Version.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/services/ManipulationService.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/services/UpdateCheckService.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/tasks/IconDownloadTask.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/tasks/ModDownloadTask.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/tasks/ModRemovalTask.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/tasks/ModUpdateTask.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/util/FabricMods.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/util/InstallationUtil.java delete mode 100644 src/main/java/xyz/deathsgun/modmanager/util/MarkdownPreprocessor.java rename src/main/{java/xyz/deathsgun/modmanager/api/mod/Category.java => kotlin/xyz/deathsgun/modmanager/Initializer.kt} (78%) rename src/main/{java/xyz/deathsgun/modmanager/providers/modrinth/model/License.java => kotlin/xyz/deathsgun/modmanager/ModManager.kt} (74%) rename src/main/{java/xyz/deathsgun/modmanager/gui/widget/better/IListScreen.java => kotlin/xyz/deathsgun/modmanager/api/http/CategoriesResult.kt} (63%) rename src/main/{java/xyz/deathsgun/modmanager/api/mod/VersionType.java => kotlin/xyz/deathsgun/modmanager/api/http/ModResult.kt} (66%) create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/api/http/ModsResult.kt rename src/main/{java/xyz/deathsgun/modmanager/api/provider/Sorting.java => kotlin/xyz/deathsgun/modmanager/api/http/VersionResult.kt} (64%) create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Asset.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Category.kt rename src/main/{java/xyz/deathsgun/modmanager/api/mod/ModVersion.java => kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt} (66%) rename src/main/{java/xyz/deathsgun/modmanager/api/mod/ModState.java => kotlin/xyz/deathsgun/modmanager/api/mod/State.kt} (84%) create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Version.kt rename src/main/{java/xyz/deathsgun/modmanager/api/mod/Asset.java => kotlin/xyz/deathsgun/modmanager/api/mod/VersionType.kt} (84%) rename src/main/{java/xyz/deathsgun/modmanager/api/provider/IModProvider.java => kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt} (56%) create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModUpdateProvider.kt rename src/main/{java/xyz/deathsgun/modmanager/api/manipulation/TaskCallback.java => kotlin/xyz/deathsgun/modmanager/api/provider/Sorting.kt} (81%) create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt rename src/main/{java/xyz/deathsgun/modmanager/api/mod/SummarizedMod.java => kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/DetailedMod.kt} (61%) create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModResult.kt rename src/main/{java/xyz/deathsgun/modmanager/api/mod/DetailedMod.java => kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModrinthVersion.kt} (50%) create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/SearchResult.kt create mode 100644 src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt diff --git a/build.gradle b/build.gradle index 45c09b9..bd45a60 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,26 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + //file:noinspection GroovyAssignabilityCheck //file:noinspection GroovyAccessibility plugins { id "fabric-loom" version "0.9-SNAPSHOT" id "com.modrinth.minotaur" version "1.2.1" + id "org.jetbrains.kotlin.jvm" version "1.5.30" + id 'org.jetbrains.kotlin.plugin.serialization' version '1.5.30' } sourceCompatibility = JavaVersion.VERSION_16 @@ -17,14 +35,17 @@ repositories { name = "TerraformersMC" url = "https://maven.terraformersmc.com/releases/" } + mavenCentral() } dependencies { minecraft "com.mojang:minecraft:${project.minecraft_version}" mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" - + modImplementation "net.fabricmc:fabric-language-kotlin:${project.fabric_kotlin_version}" modImplementation "com.terraformersmc:modmenu:${project.modmenu_version}" + + testImplementation "org.jetbrains.kotlin:kotlin-test" } processResources { @@ -36,12 +57,24 @@ processResources { } } +test { + useJUnitPlatform() +} + tasks.withType(JavaCompile).configureEach { it.options.encoding = "UTF-8" // Minecraft 1.17 (21w19a) upwards uses Java 16. it.options.release = 16 } +compileKotlin { + kotlinOptions.jvmTarget = "16" +} + +compileTestKotlin { + kotlinOptions.jvmTarget = "16" +} + java { withSourcesJar() } @@ -62,12 +95,12 @@ task publishModrinth(type: TaskModrinthUpload) { } token = System.getenv("MODRINTH") - projectId = '6kq7BzRK' + projectId = "6kq7BzRK" versionNumber = version uploadFile = remapJar versionType = VersionType.ALPHA addGameVersion(project.minecraft_version) - addLoader('fabric') + addLoader("fabric") addDependency("E4QBMVtO", Dependency.DependencyType.REQUIRED) - changelog = file("CHANGELOG.md").getText('UTF-8') -} + changelog = file("CHANGELOG.md").getText("UTF-8") +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 23134ed..14073e5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,6 +16,7 @@ # Done to increase the memory available to gradle. org.gradle.jvmargs=-Xms2G -Xmx4G + # Fabric Properties # check this on https://modmuss50.me/fabric.html minecraft_version=1.17.1 @@ -27,3 +28,6 @@ maven_group=xyz.deathsgun archives_base_name=modmanager # Dependencies modmenu_version=2.0.5 +# Kotlin +kotlin_version=1.5.30 +fabric_kotlin_version=1.6.4+kotlin.1.5.30 \ No newline at end of file diff --git a/src/main/java/xyz/deathsgun/modmanager/ModManager.java b/src/main/java/xyz/deathsgun/modmanager/ModManager.java deleted file mode 100644 index 7192d5e..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/ModManager.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager; - -import com.terraformersmc.modmenu.api.ModMenuApi; -import net.fabricmc.api.ClientModInitializer; -import net.fabricmc.api.EnvType; -import net.fabricmc.api.Environment; -import net.fabricmc.loader.api.ModContainer; -import xyz.deathsgun.modmanager.api.mod.ModState; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; -import xyz.deathsgun.modmanager.api.provider.IModProvider; -import xyz.deathsgun.modmanager.manager.IconManager; -import xyz.deathsgun.modmanager.manager.ModManipulationManager; -import xyz.deathsgun.modmanager.providers.modrinth.Modrinth; -import xyz.deathsgun.modmanager.services.ManipulationService; -import xyz.deathsgun.modmanager.services.UpdateCheckService; -import xyz.deathsgun.modmanager.util.FabricMods; - -import java.util.ArrayList; -import java.util.Optional; - -@Environment(EnvType.CLIENT) -public class ModManager implements ClientModInitializer, ModMenuApi { - - private static final String currentProvider = "Modrinth"; - private static final ArrayList modProviders = new ArrayList<>(); - private static final ManipulationService manipulationService = new ManipulationService(); - private static final IconManager iconManager = new IconManager(); - private static final ModManipulationManager modManipulationManager = new ModManipulationManager(); - private static final UpdateCheckService updateCheckService = new UpdateCheckService(); - - public static void registerModProvider(IModProvider provider) { - ModManager.modProviders.removeIf(value -> value.getName().equals(provider.getName())); - ModManager.modProviders.add(provider); - } - - public static IModProvider getModProvider() { - return modProviders.stream().filter(iModProvider -> iModProvider.getName().equals(currentProvider)).findFirst().orElse(null); - } - - public static IconManager getIconManager() { - return iconManager; - } - - public static ModManipulationManager getModManipulationManager() { - return modManipulationManager; - } - - public static UpdateCheckService getUpdateChecker() { - return updateCheckService; - } - - public static ManipulationService getManipulationService() { - return manipulationService; - } - - public static String getVersion() { - return "0.1.0"; - } - - public static ModState getState(SummarizedMod mod) { - Optional installedMod = FabricMods.getModContainerByMod(mod); - if (installedMod.isEmpty()) { - return getModManipulationManager().isInstalled(mod) ? ModState.INSTALLED : ModState.DOWNLOADABLE; - } - if (getModManipulationManager().isMarkedUninstalled(mod)) { - return ModState.DOWNLOADABLE; - } - if (getModManipulationManager().isMarkedUpdated(mod)) { - return ModState.INSTALLED; - } - return updateCheckService.isUpdateAvailable(mod, installedMod.get().getMetadata()) ? ModState.OUTDATED : ModState.INSTALLED; - } - - @Override - public void onInitializeClient() { - registerModProvider(new Modrinth()); - updateCheckService.start(); - } - -} diff --git a/src/main/java/xyz/deathsgun/modmanager/api/manipulation/ManipulationTask.java b/src/main/java/xyz/deathsgun/modmanager/api/manipulation/ManipulationTask.java deleted file mode 100644 index 9e3bf8f..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/api/manipulation/ManipulationTask.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.api.manipulation; - -import net.fabricmc.loader.api.FabricLoader; -import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; - -public abstract class ManipulationTask { - - protected final String id; - protected final SummarizedMod subject; - private final @Nullable TaskCallback taskCallback; - private final boolean debugMode; - protected Logger logger; - protected TaskState state = TaskState.SCHEDULED; - private Exception error; - - public ManipulationTask(@NotNull String id, @NotNull SummarizedMod subject, @Nullable TaskCallback taskCallback) { - this.id = id; - this.subject = subject; - this.taskCallback = taskCallback; - this.debugMode = FabricLoader.getInstance().isDevelopmentEnvironment(); - } - - protected abstract void execute() throws Exception; - - public final void executeTask() { - try { - this.state = TaskState.RUNNING; - execute(); - this.state = TaskState.FINISHED; - if (taskCallback != null) { - taskCallback.onTaskFinish(this); - } - } catch (Exception e) { - this.error = e; - logger.error("Error while executing task for {}", subject.slug(), e); - this.state = TaskState.FAILED; - if (taskCallback != null) { - taskCallback.onTaskFinish(this); - } - } - } - - public String getId() { - return id; - } - - public TaskState getState() { - return state; - } - - public SummarizedMod getSubject() { - return subject; - } - - public void debug(String message, Object... o) { - if (!debugMode) { - return; - } - logger.info(message, o); - } - - public Exception getException() { - return error; - } - - public enum TaskState { - SCHEDULED, RUNNING, FINISHED, FAILED - } - - -} diff --git a/src/main/java/xyz/deathsgun/modmanager/api/manipulation/NetworkTask.java b/src/main/java/xyz/deathsgun/modmanager/api/manipulation/NetworkTask.java deleted file mode 100644 index 6ab55a2..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/api/manipulation/NetworkTask.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.api.manipulation; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import xyz.deathsgun.modmanager.ModManager; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; - -import java.net.http.HttpClient; -import java.net.http.HttpRequest; - -public abstract class NetworkTask extends ManipulationTask { - - protected final HttpClient http; - - public NetworkTask(@NotNull String id, @NotNull SummarizedMod subject, @Nullable TaskCallback taskCallback) { - super(id, subject, taskCallback); - this.http = HttpClient.newHttpClient(); - } - - public HttpRequest build(HttpRequest.Builder builder) { - return builder.setHeader("User-Agent", "ModManager " + ModManager.getVersion()).build(); - } - -} diff --git a/src/main/java/xyz/deathsgun/modmanager/gui/ModDetailScreen.java b/src/main/java/xyz/deathsgun/modmanager/gui/ModDetailScreen.java deleted file mode 100644 index 680b38e..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/gui/ModDetailScreen.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.gui; - -import com.mojang.blaze3d.systems.RenderSystem; -import com.terraformersmc.modmenu.util.DrawingUtil; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.font.TextRenderer; -import net.minecraft.client.gui.DrawableHelper; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.screen.ScreenTexts; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.text.*; -import net.minecraft.util.Identifier; -import xyz.deathsgun.modmanager.ModManager; -import xyz.deathsgun.modmanager.api.manipulation.ManipulationTask; -import xyz.deathsgun.modmanager.api.mod.Category; -import xyz.deathsgun.modmanager.api.mod.DetailedMod; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; -import xyz.deathsgun.modmanager.gui.widget.DescriptionWidget; - -import java.util.Objects; -import java.util.regex.Pattern; - -import static xyz.deathsgun.modmanager.gui.widget.ModListEntry.LOADING_ICON; -import static xyz.deathsgun.modmanager.gui.widget.ModListEntry.UNKNOWN_ICON; - -public class ModDetailScreen extends Screen { - - private static final Pattern HTML_PATTERN = Pattern.compile("<.*?>.*?|"); - final SummarizedMod summarizedMod; - private final Screen previousScreen; - private ButtonWidget actionButton; - private DetailedMod detailedMod; - private DescriptionWidget descriptionWidget; - Exception exception; - - public ModDetailScreen(Screen previousScreen, SummarizedMod mod) { - super(new LiteralText(mod.name())); - this.previousScreen = previousScreen; - this.summarizedMod = mod; - } - - @Override - protected void init() { - super.init(); - try { - detailedMod = ModManager.getModProvider().getMod(summarizedMod.id()); - } catch (Exception e) { - e.printStackTrace(); - Objects.requireNonNull(this.client).setScreen(new ModManagerErrorScreen(previousScreen, e)); - return; - } - int buttonX = this.width / 8; - String text = detailedMod.body().replaceAll("\\u00A0", " "); - if (HTML_PATTERN.matcher(text).find()) { - text = detailedMod.description(); - } - this.descriptionWidget = this.addSelectableChild(new DescriptionWidget(client, this.width - 20, this.height - 34, 79, this.height - 30, textRenderer.fontHeight, text)); - this.descriptionWidget.setLeftPos(10); - this.addDrawableChild(new ButtonWidget(buttonX, this.height - 28, 150, 20, ScreenTexts.BACK, button -> Objects.requireNonNull(client).setScreen(previousScreen))); - - this.actionButton = this.addDrawableChild(new ButtonWidget(this.width - buttonX - 150, this.height - 28, 150, 20, new TranslatableText("modmanager.message.install"), - this::handleActionClick)); - this.updateActionButton(); - } - - private void handleActionClick(ButtonWidget buttonWidget) { - if (exception != null) { - buttonWidget.active = true; - MinecraftClient.getInstance().setScreen(new ModManagerErrorScreen(this, exception)); - return; - } - buttonWidget.active = false; - switch (ModManager.getState(summarizedMod)) { - case OUTDATED -> { - buttonWidget.setMessage(new TranslatableText("modmanager.message.updating")); - ModManager.getModManipulationManager().updateMod(summarizedMod, this::handleTaskResult); - } - case DOWNLOADABLE -> { - buttonWidget.setMessage(new TranslatableText("modmanager.message.installing")); - ModManager.getModManipulationManager().installMod(summarizedMod, this::handleTaskResult); - } - case INSTALLED -> { - buttonWidget.setMessage(new TranslatableText("modmanager.message.removing")); - ModManager.getModManipulationManager().removeMod(summarizedMod, this::handleTaskResult); - } - } - } - - private void handleTaskResult(ManipulationTask task) { - this.exception = task.getException(); - updateActionButton(); - } - - void updateActionButton() { - actionButton.active = true; - if (exception != null) { - actionButton.setMessage(new TranslatableText("modmanager.message.showError")); - return; - } - switch (ModManager.getState(summarizedMod)) { - case DOWNLOADABLE -> actionButton.setMessage(new TranslatableText("modmanager.message.install")); - case INSTALLED -> actionButton.setMessage(new TranslatableText("modmanager.message.remove")); - case OUTDATED -> actionButton.setMessage(new TranslatableText("modmanager.message.update")); - } - } - - @Override - public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { - renderBackground(matrices); - this.descriptionWidget.render(matrices, mouseX, mouseY, delta); - - int iconSize = 64; - this.bindIconTexture(); - RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); - this.bindIconTexture(); - RenderSystem.enableBlend(); - DrawableHelper.drawTexture(matrices, 20, 10, 0.0F, 0.0F, iconSize, iconSize, iconSize, iconSize); - RenderSystem.disableBlend(); - - final TextRenderer font = Objects.requireNonNull(client).textRenderer; - - MutableText trimmedTitle = new LiteralText(font.trimToWidth(detailedMod.name(), this.width - 200)); - trimmedTitle = trimmedTitle.setStyle(Style.EMPTY.withBold(true)); - - int detailsY = 15; - int textX = 20 + iconSize + 5; - - font.draw(matrices, trimmedTitle, textX, detailsY, 0xFFFFFF); - - font.draw(matrices, new TranslatableText("modmanager.details.author", summarizedMod.author()), textX, detailsY += 12, 0xFFFFFF); - - DrawingUtil.drawBadge(matrices, textX, detailsY += 12, font.getWidth(detailedMod.license()) + 6, Text.of(detailedMod.license()).asOrderedText(), 0xff6f6c6a, 0xff31302f, 0xCACACA); - - for (Category category : detailedMod.categories()) { - int textWidth = font.getWidth(category.text()) + 6; - DrawingUtil.drawBadge(matrices, textX, detailsY + 14, textWidth, category.text().asOrderedText(), 0xff6f6c6a, 0xff31302f, 0xCACACA); - textX += textWidth + 4; - } - super.render(matrices, mouseX, mouseY, delta); - } - - private void bindIconTexture() { - if (ModManager.getIconManager().isErrored(summarizedMod.id())) { - RenderSystem.setShaderTexture(0, UNKNOWN_ICON); - return; - } - Identifier icon = ModManager.getIconManager().getIconByModId(summarizedMod.id()); - if (icon == null) { - if (ModManager.getIconManager().isLoading(summarizedMod.id())) { - icon = LOADING_ICON; - } else { - ModManager.getIconManager().downloadIcon(summarizedMod); - return; - } - } - RenderSystem.setShaderTexture(0, icon); - } - - @Override - public boolean shouldCloseOnEsc() { - return true; - } - - @Override - public void onClose() { - Objects.requireNonNull(this.client).setScreen(previousScreen); - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/gui/ModManagerErrorScreen.java b/src/main/java/xyz/deathsgun/modmanager/gui/ModManagerErrorScreen.java deleted file mode 100644 index b677fde..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/gui/ModManagerErrorScreen.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.gui; - -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.screen.ScreenTexts; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.text.TranslatableText; -import org.apache.commons.lang3.exception.ExceptionUtils; -import xyz.deathsgun.modmanager.ModManager; -import xyz.deathsgun.modmanager.gui.widget.DescriptionWidget; - -import java.util.Objects; - -public class ModManagerErrorScreen extends Screen { - - private final Exception exception; - private Screen parentScreen; - private DescriptionWidget descriptionWidget; - - public ModManagerErrorScreen(Screen screen, Exception exception) { - super(new TranslatableText("modmanager.error.title")); - this.exception = exception; - this.parentScreen = screen; - } - - @Override - protected void init() { - super.init(); - String error = ExceptionUtils.getStackTrace(exception); - error = error.replaceAll("\t", "").replaceAll("\r", ""); - descriptionWidget = this.addSelectableChild(new DescriptionWidget(client, (int) (width * 0.90), height, (int) (height * 0.1), this.height - 36, 9, error)); - descriptionWidget.setLeftPos((int) (this.width * 0.05)); - this.addDrawableChild(new ButtonWidget(this.width / 2 - 154, this.height - 28, 150, 20, ScreenTexts.CANCEL, button -> this.onClose())); - this.addDrawableChild(new ButtonWidget(this.width / 2, this.height - 28, 150, 20, - new TranslatableText("modmanager.message.goBackAndTryAgain"), button -> { - if (this.parentScreen instanceof ModDetailScreen) { - this.clearErrors(); - } - this.onClose(); - })); - } - - private void clearErrors() { - ModDetailScreen screen = (ModDetailScreen) parentScreen; - screen.exception = null; - ModManager.getManipulationService().removeTasks(screen.summarizedMod.id()); - screen.updateActionButton(); - this.parentScreen = screen; - } - - @Override - public void onClose() { - Objects.requireNonNull(client).setScreen(parentScreen); - } - - @Override - public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { - renderBackground(matrices); - this.descriptionWidget.render(matrices, mouseX, mouseY, delta); - super.render(matrices, mouseX, mouseY, delta); - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.java b/src/main/java/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.java deleted file mode 100644 index f73b6ee..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.gui; - -import net.minecraft.client.font.TextRenderer; -import net.minecraft.client.gui.Element; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.gui.widget.TextFieldWidget; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.text.TranslatableText; -import org.jetbrains.annotations.Nullable; -import org.lwjgl.glfw.GLFW; -import xyz.deathsgun.modmanager.gui.widget.CategoryListEntry; -import xyz.deathsgun.modmanager.gui.widget.CategoryListWidget; -import xyz.deathsgun.modmanager.gui.widget.ModListEntry; -import xyz.deathsgun.modmanager.gui.widget.ModListWidget; -import xyz.deathsgun.modmanager.gui.widget.better.IListScreen; - -import java.util.Objects; - -public class ModsOverviewScreen extends Screen implements IListScreen { - - private final Screen previousScreen; - private ModListWidget modListWidget; - private ModListEntry selectedMod; - private CategoryListWidget categoryListWidget; - private CategoryListEntry selectedCategory; - private TextFieldWidget searchBox; - private String query; - private int paneWidth; - private int rightPaneX; - private ButtonWidget previousPage; - private ButtonWidget nextPage; - - public ModsOverviewScreen(Screen previousScreen) { - super(new TranslatableText("modmanager.title")); - this.previousScreen = previousScreen; - } - - @Override - protected void init() { - Objects.requireNonNull(this.client).keyboard.setRepeatEvents(true); - paneWidth = this.width / 4; - rightPaneX = this.paneWidth + 10; - int searchBoxWidth = paneWidth + 40; - this.searchBox = this.addSelectableChild(new TextFieldWidget(textRenderer, 5, 5, searchBoxWidth, 20, new TranslatableText("modmanager.message.search"))); - this.searchBox.setChangedListener(this::handleSearch); - - this.categoryListWidget = this.addSelectableChild(new CategoryListWidget(this.client, paneWidth, this.height, 30, this.height - 10, 14, this)); - this.categoryListWidget.setLeftPos(0); - - int modListWidth = this.width - paneWidth - 20; - this.modListWidget = this.addSelectableChild(new ModListWidget(this.client, modListWidth, this.height, 30, this.height - 40, 36, this)); - this.modListWidget.setLeftPos(rightPaneX); - this.categoryListWidget.init(); - this.modListWidget.init(); - - this.previousPage = this.addDrawableChild(new ButtonWidget(rightPaneX, this.height - 30, modListWidth / 2 - 10, 20, - new TranslatableText("modmanager.page.previous"), button -> this.modListWidget.showPreviousPage())); - this.nextPage = this.addDrawableChild(new ButtonWidget(rightPaneX + modListWidth / 2, this.height - 30, modListWidth / 2, 20, - new TranslatableText("modmanager.page.next"), button -> this.modListWidget.showNextPage())); - } - - private void handleSearch(String query) { - this.query = query; - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (keyCode == GLFW.GLFW_KEY_ENTER && this.searchBox.isFocused()) { - this.modListWidget.searchMods(query); - } - return super.keyPressed(keyCode, scanCode, modifiers); - } - - @Override - public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { - this.renderBackground(matrices); - TextRenderer font = Objects.requireNonNull(client).textRenderer; - font.draw(matrices, new TranslatableText("modmanager.categories"), 5, 29 - font.fontHeight, 0xFFFFFF); - this.categoryListWidget.render(matrices, mouseX, mouseY, delta); - this.modListWidget.render(matrices, mouseX, mouseY, delta); - this.searchBox.render(matrices, mouseX, mouseY, delta); - super.render(matrices, mouseX, mouseY, delta); - } - - @Override - public void tick() { - this.searchBox.tick(); - this.previousPage.active = this.modListWidget.getCurrentPage() > 0; - this.nextPage.active = this.modListWidget.getEntryCount() >= this.modListWidget.getLimit(); - } - - @Override - public void onClose() { - super.onClose(); - this.modListWidget.close(); - Objects.requireNonNull(this.client).setScreen(this.previousScreen); - } - - @Override - public void updateSelectedEntry(Object widget, E entry) { - if (widget == this.categoryListWidget) { - if (entry != null) { - this.searchBox.setText(""); - this.searchBox.setTextFieldFocused(false); - this.selectedCategory = (CategoryListEntry) entry; - this.modListWidget.setCategory(selectedCategory.getCategory(), false); - } - } - if (widget == this.modListWidget) { - if (entry != null) { - if (this.selectedMod == entry) { - Objects.requireNonNull(this.client).setScreen(new ModDetailScreen(this, ((ModListEntry) entry).getMod())); - return; - } - if (entry instanceof ModListEntry) { - this.selectedMod = (ModListEntry) entry; - } - } - } - } - - @Nullable - @Override - public Element getFocused() { - return super.getFocused(); - } - - @SuppressWarnings("unchecked") - @Override - public E getEntry(Object widget) { - if (widget == this.categoryListWidget) { - return (E) this.selectedCategory; - } - return null; - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/gui/widget/CategoryListEntry.java b/src/main/java/xyz/deathsgun/modmanager/gui/widget/CategoryListEntry.java deleted file mode 100644 index ce51401..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/gui/widget/CategoryListEntry.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.gui.widget; - -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.font.TextRenderer; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.text.OrderedText; -import net.minecraft.text.Text; -import net.minecraft.util.Language; -import xyz.deathsgun.modmanager.api.mod.Category; -import xyz.deathsgun.modmanager.gui.widget.better.BetterListWidget; - -public class CategoryListEntry extends BetterListWidget.BetterListEntry { - - private final Category category; - private final MinecraftClient client = MinecraftClient.getInstance(); - - public CategoryListEntry(CategoryListWidget list, Category category) { - super(list, category.text()); - this.category = category; - } - - @Override - public void render(MatrixStack matrices, int index, int y, int x, int rowWidth, int rowHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { - TextRenderer font = this.client.textRenderer; - Text text = category.text(); - if (this.list.isSelectedEntry(this)) { - text = text.getWithStyle(text.getStyle().withBold(true)).get(0); - } - OrderedText trimmedText = Language.getInstance().reorder(font.trimToWidth(text, rowWidth - 10)); - font.draw(matrices, trimmedText, x + 3, y + 1, 0xFFFFFF); - } - - @Override - public String id() { - return category.id(); - } - - @Override - public Text getTitle() { - return category.text(); - } - - public Category getCategory() { - return category; - } - - @Override - public Text getNarration() { - return null; - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.java b/src/main/java/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.java deleted file mode 100644 index 7174de8..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.gui.widget; - -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.text.TranslatableText; -import xyz.deathsgun.modmanager.ModManager; -import xyz.deathsgun.modmanager.api.mod.Category; -import xyz.deathsgun.modmanager.gui.ModManagerErrorScreen; -import xyz.deathsgun.modmanager.gui.ModsOverviewScreen; -import xyz.deathsgun.modmanager.gui.widget.better.BetterListWidget; - -import java.util.ArrayList; - -public class CategoryListWidget extends BetterListWidget { - - private ArrayList categories; - - public CategoryListWidget(MinecraftClient client, int width, int height, int top, int bottom, int itemHeight, ModsOverviewScreen parentScreen) { - super(client, width, height, top, bottom, itemHeight, parentScreen); - setScrollAmount(0.0 * Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4))); - } - - @Override - public void init() { - try { - this.categories = new ArrayList<>(); - if (ModManager.getUpdateChecker().updatesAvailable()) { - this.addEntry(new CategoryListEntry(this, new Category( - "updatable", - new TranslatableText("modmanager.categories.updatable") - ))); - } - ModManager.getModProvider().getCategories().forEach(category -> this.addEntry(new CategoryListEntry(this, category))); - if (parent.getEntry(this) != null) { - setSelected(parent.getEntry(this)); - return; - } - setSelected(this.getEntry(0)); - } catch (Exception e) { - client.setScreen(new ModManagerErrorScreen((Screen) parent, e)); - e.printStackTrace(); - } - } - - @Override - public void setSelected(CategoryListEntry entry) { - super.setSelected(entry); - selectedId = entry.getCategory().id(); - parent.updateSelectedEntry(this, getSelectedOrNull()); - } - - @Override - public int addEntry(CategoryListEntry entry) { - if (this.categories.contains(entry.getCategory())) { - return 0; - } - this.categories.add(entry.getCategory()); - return super.addEntry(entry); - } - - @Override - protected boolean removeEntry(CategoryListEntry entry) { - this.categories.remove(entry.getCategory()); - return super.removeEntry(entry); - } - - @Override - protected CategoryListEntry remove(int index) { - this.categories.remove(getEntry(index).getCategory()); - return super.remove(index); - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/gui/widget/DescriptionListEntry.java b/src/main/java/xyz/deathsgun/modmanager/gui/widget/DescriptionListEntry.java deleted file mode 100644 index 1098657..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/gui/widget/DescriptionListEntry.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.gui.widget; - -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.font.TextRenderer; -import net.minecraft.client.gui.widget.EntryListWidget; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.text.ClickEvent; -import net.minecraft.text.OrderedText; -import net.minecraft.text.Text; -import net.minecraft.util.Util; - -public class DescriptionListEntry extends EntryListWidget.Entry { - - private final DescriptionWidget widget; - private final OrderedText orderedText; - private final TextRenderer textRenderer; - private Text text; - private int x = 0; - - public DescriptionListEntry(DescriptionWidget widget, Text text) { - this(widget, text.asOrderedText()); - this.text = text; - } - - public DescriptionListEntry(DescriptionWidget widget, OrderedText orderedText) { - this.textRenderer = MinecraftClient.getInstance().textRenderer; - this.widget = widget; - this.orderedText = orderedText; - } - - @Override - public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { - if (y >= widget.getBottom() - textRenderer.fontHeight + 2) { - return; - } - this.x = x; - textRenderer.draw(matrices, orderedText, x, y, 0xFFFFFF); - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (text == null) { - return super.mouseClicked(mouseX, mouseY, button); - } - if (isMouseOver(mouseX, mouseY)) { - ClickEvent event = text.getStyle().getClickEvent(); - if (event == null || event.getAction() != ClickEvent.Action.OPEN_URL) { - return super.mouseClicked(mouseX, mouseY, button); - } - Util.getOperatingSystem().open(event.getValue()); - } - return super.mouseClicked(mouseX, mouseY, button); - } - - @Override - public boolean isMouseOver(double mouseX, double mouseY) { - return super.isMouseOver(mouseX, mouseY) && x + textRenderer.getWidth(text) >= mouseX; - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.java b/src/main/java/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.java deleted file mode 100644 index 85641fd..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.gui.widget; - -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.font.TextRenderer; -import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; -import net.minecraft.client.gui.widget.EntryListWidget; -import net.minecraft.text.LiteralText; -import net.minecraft.text.MutableText; -import net.minecraft.text.OrderedText; -import xyz.deathsgun.modmanager.util.MarkdownPreprocessor; - -import java.util.List; - -public class DescriptionWidget extends EntryListWidget { - - private final String text; - - public DescriptionWidget(MinecraftClient client, int width, int height, int top, int bottom, int itemHeight, String text) { - super(client, width, height, top, bottom, itemHeight); - this.text = text; - init(); - } - - private void init() { - MutableText[] lines = MarkdownPreprocessor.processText(text); - TextRenderer textRenderer = client.textRenderer; - for (MutableText line : lines) { - if (textRenderer.getWidth(line) >= width - 10) { - List texts = textRenderer.wrapLines(line, width - 10); - for (OrderedText wrappedLine : texts) { - addEntry(new DescriptionListEntry(this, wrappedLine)); - } - continue; - } - addEntry(new DescriptionListEntry(this, line)); - } - addEntry(new DescriptionListEntry(this, new LiteralText(""))); - } - - @Override - public DescriptionListEntry getSelectedOrNull() { - return null; - } - - @Override - public int getRowWidth() { - return this.width - 10; - } - - @Override - protected int getScrollbarPositionX() { - return this.width - 6 + left; - } - - @Override - public void appendNarrations(NarrationMessageBuilder builder) { - // Better not read this thing - } - - public int getBottom() { - return bottom; - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/gui/widget/ModListEntry.java b/src/main/java/xyz/deathsgun/modmanager/gui/widget/ModListEntry.java deleted file mode 100644 index d95cbda..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/gui/widget/ModListEntry.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.gui.widget; - -import com.mojang.blaze3d.systems.RenderSystem; -import com.terraformersmc.modmenu.util.DrawingUtil; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.font.TextRenderer; -import net.minecraft.client.gui.DrawableHelper; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.text.*; -import net.minecraft.util.Identifier; -import net.minecraft.util.Language; -import org.jetbrains.annotations.NotNull; -import xyz.deathsgun.modmanager.ModManager; -import xyz.deathsgun.modmanager.api.mod.DetailedMod; -import xyz.deathsgun.modmanager.api.mod.ModState; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; -import xyz.deathsgun.modmanager.gui.widget.better.BetterListWidget; - -import java.util.concurrent.CompletableFuture; - -public class ModListEntry extends BetterListWidget.BetterListEntry { - - public static final Identifier UNKNOWN_ICON = new Identifier("textures/misc/unknown_pack.png"); - public static final Identifier LOADING_ICON = new Identifier("modmanager", "textures/gui/loading.png"); - - private final SummarizedMod mod; - private final MinecraftClient client = MinecraftClient.getInstance(); - private ModState modState = ModState.CHECKING; - - public ModListEntry(ModListWidget list, @NotNull SummarizedMod mod) { - super(list, new LiteralText(mod.name())); - this.mod = mod; - CompletableFuture.runAsync(() -> this.modState = ModManager.getState(this.mod)); - } - - public ModListEntry(ModListWidget list, DetailedMod mod) { - super(list, new LiteralText(mod.name())); - this.mod = mod.toSummarizedMod(); - // When calling this it's almost likely because of the updatable mods overview - this.modState = ModState.OUTDATED; - } - - @Override - public String id() { - return mod.id(); - } - - @Override - public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { - int iconSize = 32; - RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); - this.bindIconTexture(); - RenderSystem.enableBlend(); - DrawableHelper.drawTexture(matrices, x, y, 0.0F, 0.0F, iconSize, iconSize, iconSize, iconSize); - RenderSystem.disableBlend(); - Text name = new LiteralText(mod.name()); - StringVisitable trimmedName = name; - int maxNameWidth = entryWidth - iconSize - 3; - TextRenderer font = this.client.textRenderer; - - int primaryColor = 0xFFFFFF; - int secondaryColor = 0xFFFFFF; - OrderedText badgeText = null; - if (modState == ModState.INSTALLED) { - primaryColor = 0xff0e2a55; - secondaryColor = 0xff2b4b7c; - badgeText = new TranslatableText("modmanager.badge.installed").asOrderedText(); - maxNameWidth -= font.getWidth(badgeText) + 6; - } else if (modState == ModState.OUTDATED) { - primaryColor = 0xff530C17; - secondaryColor = 0xff841426; - badgeText = new TranslatableText("modmanager.badge.outdated").asOrderedText(); - maxNameWidth -= font.getWidth(badgeText) + 6; - } - - int textWidth = font.getWidth(name); - if (textWidth > maxNameWidth) { - StringVisitable ellipsis = StringVisitable.plain("..."); - trimmedName = StringVisitable.concat(font.trimToWidth(name, maxNameWidth - font.getWidth(ellipsis)), ellipsis); - } - font.draw(matrices, Language.getInstance().reorder(trimmedName), x + iconSize + 3, y + 1, 0xFFFFFF); - if (badgeText != null) { - DrawingUtil.drawBadge(matrices, x + iconSize + 3 + textWidth + 3, y + 1, font.getWidth(badgeText) + 6, badgeText, secondaryColor, primaryColor, 0xFFFFFF); - } - - DrawingUtil.drawWrappedString(matrices, mod.description(), (x + iconSize + 3 + 4), (y + client.textRenderer.fontHeight + 4), entryWidth - iconSize - 7, 2, 0x808080); - } - - - private void bindIconTexture() { - if (ModManager.getIconManager().isErrored(mod.id())) { - RenderSystem.setShaderTexture(0, UNKNOWN_ICON); - return; - } - Identifier icon = ModManager.getIconManager().getIconByModId(mod.id()); - if (icon == null) { - if (ModManager.getIconManager().isLoading(mod.id())) { - icon = LOADING_ICON; - } else { - ModManager.getIconManager().downloadIcon(mod); - return; - } - } - RenderSystem.setShaderTexture(0, icon); - } - - public SummarizedMod getMod() { - return mod; - } - - @Override - public Text getNarration() { - return getTitle(); - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/gui/widget/ModListWidget.java b/src/main/java/xyz/deathsgun/modmanager/gui/widget/ModListWidget.java deleted file mode 100644 index ad8cced..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/gui/widget/ModListWidget.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.gui.widget; - -import net.minecraft.client.MinecraftClient; -import xyz.deathsgun.modmanager.ModManager; -import xyz.deathsgun.modmanager.api.mod.Category; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; -import xyz.deathsgun.modmanager.gui.ModsOverviewScreen; -import xyz.deathsgun.modmanager.gui.widget.better.BetterListWidget; - -import java.util.ArrayList; -import java.util.Objects; - -public class ModListWidget extends BetterListWidget { - - private final int limit = 20; - private final ArrayList mods = new ArrayList<>(); - private int page = 0; - private Category category; - private String query; - - public ModListWidget(MinecraftClient client, int width, int height, int top, int bottom, int itemHeight, ModsOverviewScreen parentScreen) { - super(client, width, height, top, bottom, itemHeight, parentScreen); - setScrollAmount(0.0 * Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4))); - } - - @Override - public void init() { - } - - public void setCategory(Category category, boolean force) { - setScrollAmount(0.0); - if (this.category != null && Objects.equals(this.category.id(), category.id()) && !force) { - return; - } - this.category = category; - this.clearMods(); - if (category.id().equals("updatable")) { - this.addUpdatableMods(); - return; - } - try { - ModManager.getModProvider().getMods(category, page, limit) - .forEach(mod -> this.addEntry(new ModListEntry(this, mod))); - } catch (Exception e) { - e.printStackTrace(); - } - } - - private void addUpdatableMods() { - ModManager.getUpdateChecker().getUpdatableMods().stream() - .filter(detailedMod -> !ModManager.getModManipulationManager().isMarkedUpdated(detailedMod.toSummarizedMod())) - .forEach(mod -> this.addEntry(new ModListEntry(this, mod))); - } - - public void searchMods(String query) { - if (query == null || "".equals(query)) { - return; - } - setScrollAmount(0.0); - this.clearMods(); - try { - this.category = null; - ModManager.getModProvider().getMods(query, page, limit) - .forEach(mod -> this.addEntry(new ModListEntry(this, mod))); - this.query = query; - } catch (Exception e) { - e.printStackTrace(); - } - } - - public int getLimit() { - return limit; - } - - private void clearMods() { - this.mods.clear(); - for (int i = 0; i < this.getEntryCount(); i++) { - this.remove(i); - } - this.clearEntries(); - } - - @Override - public int addEntry(ModListEntry entry) { - if (mods.contains(entry.getMod())) { - return 0; - } - mods.add(entry.getMod()); - int i = super.addEntry(entry); - if (entry.id().equals(selectedId)) { - setSelected(entry); - } - return i; - } - - @Override - protected boolean removeEntry(ModListEntry entry) { - mods.remove(entry.getMod()); - return super.removeEntry(entry); - } - - @Override - protected ModListEntry remove(int index) { - ModListEntry entry = getEntry(index); - mods.remove(entry.getMod()); - return super.remove(index); - } - - public void showNextPage() { - this.page++; - if (this.category != null) { - this.setCategory(category, true); - return; - } - this.searchMods(this.query); - } - - public void showPreviousPage() { - this.page--; - if (this.page < 0) { - page = 0; - } - if (this.category != null) { - this.setCategory(category, true); - return; - } - this.searchMods(this.query); - } - - public int getCurrentPage() { - return page; - } - - public void close() { - ModManager.getIconManager().destroyAllIcons(); - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/gui/widget/ModManagerTexturedButtonWidget.java b/src/main/java/xyz/deathsgun/modmanager/gui/widget/ModManagerTexturedButtonWidget.java deleted file mode 100644 index ea6ae14..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/gui/widget/ModManagerTexturedButtonWidget.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.gui.widget; - -import com.terraformersmc.modmenu.gui.widget.ModMenuTexturedButtonWidget; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; - -public class ModManagerTexturedButtonWidget extends ModMenuTexturedButtonWidget { - - public ModManagerTexturedButtonWidget(int x, int y, int width, int height, int u, int v, Identifier texture, int uWidth, int vHeight, PressAction onPress, Text message) { - super(x, y, width, height, u, v, texture, uWidth, vHeight, onPress, message); - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/gui/widget/better/BetterListWidget.java b/src/main/java/xyz/deathsgun/modmanager/gui/widget/better/BetterListWidget.java deleted file mode 100644 index 2e6c333..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/gui/widget/better/BetterListWidget.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.gui.widget.better; - -import com.mojang.blaze3d.systems.RenderSystem; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; -import net.minecraft.client.gui.widget.AlwaysSelectedEntryListWidget; -import net.minecraft.client.render.*; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.text.Text; -import net.minecraft.util.math.MathHelper; -import net.minecraft.util.math.Matrix4f; -import org.jetbrains.annotations.Nullable; - -import java.util.Objects; - -/** - * Using ModMenu's implementation because the implementation of - * Mojang's is broken. {@link com.terraformersmc.modmenu.gui.ModsScreen}. - * All credits for this code go to the Terraformers - */ -public abstract class BetterListWidget> extends AlwaysSelectedEntryListWidget { - - protected final IListScreen parent; - protected String selectedId = null; - private boolean scrolling; - - public BetterListWidget(MinecraftClient client, int width, int height, int top, int bottom, int itemHeight, IListScreen parent) { - super(client, width, height, top, bottom, itemHeight); - this.parent = parent; - } - - public abstract void init(); - - @Override - protected boolean isFocused() { - return parent.getFocused() == this; - } - - @Override - public void setSelected(@Nullable E entry) { - super.setSelected(entry); - if (entry != null) { - selectedId = entry.id(); - } else { - selectedId = null; - } - parent.updateSelectedEntry(this, getSelectedOrNull()); - } - - @Override - protected boolean isSelectedEntry(int index) { - return Objects.equals(getEntry(index).id(), selectedId); - } - - @Override - protected int addEntry(E entry) { - int i = super.addEntry(entry); - if (entry.id().equals(selectedId)) { - setSelected(entry); - } - return i; - } - - @Override - protected void renderList(MatrixStack matrices, int x, int y, int mouseX, int mouseY, float delta) { - int itemCount = this.getEntryCount(); - Tessellator tessellator = Tessellator.getInstance(); - BufferBuilder buffer = tessellator.getBuffer(); - - for (int index = 0; index < itemCount; ++index) { - int entryTop = this.getRowTop(index) + 2; - int entryBottom = this.getRowTop(index) + this.itemHeight; - if (entryBottom >= this.top && entryTop <= this.bottom) { - int entryHeight = this.itemHeight - 4; - BetterListEntry entry = this.getEntry(index); - int rowWidth = this.getRowWidth(); - int entryLeft; - if (this.isSelectedEntry(index)) { - entryLeft = getRowLeft() - 2; - int selectionRight = x + rowWidth + 2; - RenderSystem.disableTexture(); - float float_2 = this.isFocused() ? 1.0F : 0.5F; - RenderSystem.setShader(GameRenderer::getPositionShader); - RenderSystem.setShaderColor(float_2, float_2, float_2, 1.0F); - Matrix4f matrix = matrices.peek().getModel(); - buffer.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION); - buffer.vertex(matrix, entryLeft, entryTop + entryHeight + 2, 0.0F).next(); - buffer.vertex(matrix, selectionRight, entryTop + entryHeight + 2, 0.0F).next(); - buffer.vertex(matrix, selectionRight, entryTop - 2, 0.0F).next(); - buffer.vertex(matrix, entryLeft, entryTop - 2, 0.0F).next(); - tessellator.draw(); - RenderSystem.setShader(GameRenderer::getPositionShader); - RenderSystem.setShaderColor(0.0F, 0.0F, 0.0F, 1.0F); - buffer.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION); - buffer.vertex(matrix, entryLeft + 1, entryTop + entryHeight + 1, 0.0F).next(); - buffer.vertex(matrix, selectionRight - 1, entryTop + entryHeight + 1, 0.0F).next(); - buffer.vertex(matrix, selectionRight - 1, entryTop - 1, 0.0F).next(); - buffer.vertex(matrix, entryLeft + 1, entryTop - 1, 0.0F).next(); - tessellator.draw(); - RenderSystem.enableTexture(); - } - - entryLeft = this.getRowLeft(); - entry.render(matrices, index, entryTop, entryLeft, rowWidth, entryHeight, mouseX, mouseY, this.isMouseOver(mouseX, mouseY) && Objects.equals(this.getEntryAtPos(mouseX, mouseY), entry), delta); - } - } - } - - @Override - protected void updateScrollingState(double double_1, double double_2, int int_1) { - super.updateScrollingState(double_1, double_2, int_1); - this.scrolling = int_1 == 0 && double_1 >= (double) this.getScrollbarPositionX() && double_1 < (double) (this.getScrollbarPositionX() + 6); - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int mouseButton) { - this.updateScrollingState(mouseX, mouseY, mouseButton); - if (!this.isMouseOver(mouseX, mouseY)) { - return false; - } else { - BetterListEntry entry = this.getEntryAtPos(mouseX, mouseY); - if (entry != null) { - if (entry.mouseClicked(mouseX, mouseY, mouseButton)) { - this.setFocused(entry); - this.setDragging(true); - return true; - } - } else if (mouseButton == 0) { - this.clickedHeader((int) (mouseX - (double) (this.left + this.width / 2 - this.getRowWidth() / 2)), (int) (mouseY - (double) this.top) + (int) this.getScrollAmount() - 4); - return true; - } - - return this.scrolling; - } - } - - public final BetterListEntry getEntryAtPos(double x, double y) { - int int_5 = MathHelper.floor(y - (double) this.top) - this.headerHeight + (int) this.getScrollAmount() - 4; - int index = int_5 / this.itemHeight; - return x < (double) this.getScrollbarPositionX() && x >= (double) getRowLeft() && x <= (double) (getRowLeft() + getRowWidth()) && index >= 0 && int_5 >= 0 && index < this.getEntryCount() ? this.children().get(index) : null; - } - - @Override - public int getRowWidth() { - return this.width - (Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4)) > 0 ? 18 : 12); - } - - @Override - public int getRowLeft() { - return left + 6; - } - - @Override - protected int getScrollbarPositionX() { - return this.left + this.width - 6; - } - - @Override - protected int getMaxPosition() { - return super.getMaxPosition() + 4; - } - - @Override - public void appendNarrations(NarrationMessageBuilder builder) { - } - - public boolean isSelectedEntry(BetterListEntry entry) { - return Objects.equals(selectedId, entry.id()); - } - - - public int getEntryCount() { - return super.getEntryCount(); - } - - public static abstract class BetterListEntry> extends AlwaysSelectedEntryListWidget.Entry { - - protected final BetterListWidget list; - private final Text title; - - public BetterListEntry(BetterListWidget list, Text title) { - this.list = list; - this.title = title; - } - - public abstract String id(); - - public Text getTitle() { - return title; - } - - @SuppressWarnings("unchecked") - @Override - public boolean mouseClicked(double v, double v1, int i) { - list.setSelected((E) this); - return true; - } - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/manager/IconManager.java b/src/main/java/xyz/deathsgun/modmanager/manager/IconManager.java deleted file mode 100644 index 0dc02bb..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/manager/IconManager.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.manager; - -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.texture.NativeImage; -import net.minecraft.client.texture.NativeImageBackedTexture; -import net.minecraft.util.Identifier; -import xyz.deathsgun.modmanager.ModManager; -import xyz.deathsgun.modmanager.api.manipulation.ManipulationTask; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; -import xyz.deathsgun.modmanager.tasks.IconDownloadTask; - -import java.util.ArrayList; -import java.util.HashMap; - -public class IconManager { - - private final HashMap icons = new HashMap<>(); - - public Identifier getIconByModId(String modId) { - return icons.get(modId); - } - - public void downloadIcon(SummarizedMod mod) { - if (this.icons.containsKey(mod.id())) { - return; - } - ModManager.getManipulationService().add(new IconDownloadTask(mod.id() + "_icon_download", mod, null)); - } - - public void registerIcon(String modId, Identifier identifier, NativeImage texture) { - MinecraftClient.getInstance().getTextureManager().registerTexture(identifier, new NativeImageBackedTexture(texture)); - this.icons.put(modId, identifier); - ModManager.getManipulationService().remove(modId + "_icon_download"); - } - - public void destroyIcon(String modId) { - Identifier identifier = this.icons.remove(modId); - MinecraftClient.getInstance().getTextureManager().destroyTexture(identifier); - } - - public boolean isErrored(String modId) { - modId += "_icon_download"; - return ModManager.getManipulationService().isState(modId, ManipulationTask.TaskState.FAILED); - } - - public boolean isLoading(String modId) { - modId += "_icon_download"; - return ModManager.getManipulationService().isState(modId, ManipulationTask.TaskState.RUNNING) || - ModManager.getManipulationService().isState(modId, ManipulationTask.TaskState.SCHEDULED); - } - - public void destroyAllIcons() { - if (this.icons.isEmpty()) { - return; - } - for (String modId : new ArrayList<>(icons.keySet())) { - destroyIcon(modId); - } - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/manager/ModManipulationManager.java b/src/main/java/xyz/deathsgun/modmanager/manager/ModManipulationManager.java deleted file mode 100644 index 4edf908..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/manager/ModManipulationManager.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.manager; - -import com.google.gson.JsonArray; -import xyz.deathsgun.modmanager.ModManager; -import xyz.deathsgun.modmanager.api.manipulation.TaskCallback; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; -import xyz.deathsgun.modmanager.tasks.ModDownloadTask; -import xyz.deathsgun.modmanager.tasks.ModRemovalTask; -import xyz.deathsgun.modmanager.tasks.ModUpdateTask; -import xyz.deathsgun.modmanager.util.FabricMods; - -import java.util.ArrayList; - -public class ModManipulationManager { - - private final ArrayList manuallyInstalled = new ArrayList<>(); - private final ArrayList uninstalledMods = new ArrayList<>(); - private final ArrayList updatedMods = new ArrayList<>(); - - public void installMod(SummarizedMod mod, TaskCallback taskCallback) { - ModManager.getManipulationService().add(new ModDownloadTask(mod.id() + "_mod_download", mod, taskCallback)); - } - - public void markManuallyInstalled(SummarizedMod mod) { - this.manuallyInstalled.add(mod.id()); - this.uninstalledMods.removeIf(s -> mod.id().equals(s)); - } - - public void markManuallyUpdated(SummarizedMod mod) { - this.updatedMods.add(mod.id()); - this.manuallyInstalled.removeIf(s -> mod.id().equals(s)); - this.uninstalledMods.removeIf(s -> mod.id().equals(s)); - } - - public void removeManuallyInstalled(SummarizedMod mod) { - this.manuallyInstalled.removeIf(s -> mod.id().equals(s)); - this.uninstalledMods.add(mod.id()); - } - - public void removeMod(SummarizedMod mod, TaskCallback taskCallback) { - ModManager.getManipulationService().add(new ModRemovalTask(mod.id() + "_mod_removal", mod, taskCallback)); - } - - public boolean isInstalled(SummarizedMod mod) { - return this.manuallyInstalled.contains(mod.id()) || FabricMods.getModContainerByMod(mod).isPresent(); - } - - public boolean isMarkedUninstalled(SummarizedMod summarizedMod) { - return this.uninstalledMods.contains(summarizedMod.id()); - } - - public void updateMod(SummarizedMod mod, TaskCallback taskCallback) { - ModManager.getManipulationService().add(new ModUpdateTask(mod.id() + "_mod_update", mod, taskCallback)); - } - - public boolean isMarkedUpdated(SummarizedMod mod) { - return this.updatedMods.contains(mod.id()); - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java b/src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java deleted file mode 100644 index 3e29c4a..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.mixin; - -import com.terraformersmc.modmenu.gui.ModsScreen; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.text.Text; -import net.minecraft.text.TranslatableText; -import net.minecraft.util.Identifier; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import xyz.deathsgun.modmanager.gui.ModsOverviewScreen; -import xyz.deathsgun.modmanager.gui.widget.ModManagerTexturedButtonWidget; - -import java.util.Objects; - -@Mixin(ModsScreen.class) -public class ModsScreenMixin extends Screen { - - private static final Identifier MODMANAGER_BUTTON_LOCATION = new Identifier("modmanager", "textures/gui/install_button.png"); - @Shadow - private int paneWidth; - - protected ModsScreenMixin(Text title) { - super(title); - } - - @Inject(method = "init", at = @At("TAIL")) - public void onInit(CallbackInfo ci) { - int searchBoxWidth = this.paneWidth - 32 - 22; - this.addDrawableChild(new ModManagerTexturedButtonWidget(this.paneWidth / 2 + searchBoxWidth / 2 + 14, - 22, 20, 20, 0, 0, MODMANAGER_BUTTON_LOCATION, 32, 64, button -> { - Objects.requireNonNull(this.client).setScreen(new ModsOverviewScreen(this)); - }, new TranslatableText("modmanager.button.open"))); - } - -} diff --git a/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java b/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java deleted file mode 100644 index f75c3ba..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.mixin; - -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.screen.TitleScreen; -import net.minecraft.client.toast.SystemToast; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.text.Text; -import net.minecraft.text.TranslatableText; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import xyz.deathsgun.modmanager.ModManager; - -import java.util.Objects; - -@Mixin(TitleScreen.class) -public abstract class TitleScreenMixin extends Screen { - - private boolean hasRun = false; - - protected TitleScreenMixin(Text title) { - super(title); - } - - @Inject(at = @At("TAIL"), method = "render") - public void onRender(MatrixStack matrices, int mouseX, int mouseY, float delta, CallbackInfo ci) { - if (hasRun) { - return; - } - hasRun = true; - if (!ModManager.getUpdateChecker().updatesAvailable()) { - return; - } - Objects.requireNonNull(client).getToastManager().add(new SystemToast(SystemToast.Type.TUTORIAL_HINT, - new TranslatableText("modmanager.toast.update.title"), - new TranslatableText("modmanager.toast.update.description", ModManager.getUpdateChecker().updatesAvailableCount()))); - } - -} diff --git a/src/main/java/xyz/deathsgun/modmanager/model/ReducedModMetadata.java b/src/main/java/xyz/deathsgun/modmanager/model/ReducedModMetadata.java deleted file mode 100644 index f0052eb..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/model/ReducedModMetadata.java +++ /dev/null @@ -1,15 +0,0 @@ -package xyz.deathsgun.modmanager.model; - -public class ReducedModMetadata { - - private String id; - private String name; - - public String getId() { - return id; - } - - public String getName() { - return name; - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.java b/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.java deleted file mode 100644 index b8fd73a..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.providers.modrinth; - -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.TranslatableText; -import org.apache.http.client.utils.URIBuilder; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.NotNull; -import xyz.deathsgun.modmanager.ModManager; -import xyz.deathsgun.modmanager.api.mod.Category; -import xyz.deathsgun.modmanager.api.mod.DetailedMod; -import xyz.deathsgun.modmanager.api.mod.ModVersion; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; -import xyz.deathsgun.modmanager.api.provider.IModProvider; -import xyz.deathsgun.modmanager.api.provider.Sorting; -import xyz.deathsgun.modmanager.providers.modrinth.model.Mod; -import xyz.deathsgun.modmanager.providers.modrinth.model.SearchResponse; -import xyz.deathsgun.modmanager.providers.modrinth.model.Version; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.stream.Collectors; - -public class Modrinth implements IModProvider { - - private final Logger logger = LogManager.getLogger("Modrinth"); - private final ArrayList categories = new ArrayList<>(); - private final HashMap> cachedCategoryRequests = new HashMap<>(); - private final Gson gson = new Gson(); - private final String baseUrl = "https://api.modrinth.com"; - private final HttpClient http = HttpClient.newHttpClient(); - - @Override - public String getName() { - return "Modrinth"; - } - - @Override - public List getCategories() throws Exception { - if (!this.categories.isEmpty()) { - return this.categories; - } - logger.info("Getting categories"); - HttpRequest request = HttpRequest.newBuilder().GET().setHeader("User-Agent", "ModManager " + ModManager.getVersion()) - .uri(URI.create(this.baseUrl + "/api/v1/tag/category")).build(); - HttpResponse response = this.http.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() != 200) { - logger.error("Received an invalid status code while getting the categories {}: {}", response.statusCode(), response.body()); - throw new Exception(response.body()); - } - ArrayList categoryStrings = gson.fromJson(response.body(), new TypeToken>() { - }.getType()); - categories.clear(); - for (String category : categoryStrings) { - this.categories.add(new Category(category, new TranslatableText(String.format("modmanager.category.%s", category)))); - } - return categories; - } - - @Override - public List getMods(Sorting sorting, int page, int limit) throws Exception { - logger.info("Getting a general list of mods"); - URIBuilder uriBuilder = new URIBuilder(this.baseUrl + "/api/v1/mod"); - uriBuilder.addParameter("index", sorting.name()); - uriBuilder.addParameter("filters", "categories=\"fabric\""); - return getSummarizedMods(page, limit, uriBuilder); - } - - @Override - public List getMods(Category category, int page, int limit) throws Exception { - logger.info("Getting category '{}' from Modrinth", category.id()); - String key = String.format("%s|%d|%d", category.id(), page, limit); - if (this.cachedCategoryRequests.containsKey(key)) { - return this.cachedCategoryRequests.get(key); - } - URIBuilder uriBuilder = new URIBuilder(this.baseUrl + "/api/v1/mod"); - uriBuilder.addParameter("filters", String.format("categories=\"fabric\" AND NOT client_side=\"unsupported\" AND categories=\"%s\"", category.id())); - List mods = getSummarizedMods(page, limit, uriBuilder); - this.cachedCategoryRequests.put(key, mods); - return mods; - } - - @Override - public List getMods(@NotNull String query, int page, int limit) throws Exception { - logger.info("Searching for '{}' in Modrinth", query); - URIBuilder uriBuilder = new URIBuilder(this.baseUrl + "/api/v1/mod"); - uriBuilder.addParameter("query", query); - uriBuilder.addParameter("filters", "categories=\"fabric\" AND NOT client_side=\"unsupported\""); - return getSummarizedMods(page, limit, uriBuilder); - } - - @NotNull - private ArrayList getSummarizedMods(int page, int limit, URIBuilder uriBuilder) throws Exception { - uriBuilder.addParameter("version", String.format("versions=%s", MinecraftClient.getInstance().getGame().getVersion().getReleaseTarget())); - uriBuilder.addParameter("offset", String.valueOf(page * limit)); - uriBuilder.addParameter("limit", String.valueOf(limit)); - HttpRequest request = HttpRequest.newBuilder().GET().setHeader("User-Agent", "ModManager " + ModManager.getVersion()) - .uri(uriBuilder.build()).build(); - HttpResponse response = this.http.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() != 200) { - throw new Exception(response.body()); - } - SearchResponse searchResponse = gson.fromJson(response.body(), SearchResponse.class); - return searchResponse.toSummarizedMods(); - } - - @Override - public DetailedMod getMod(@NotNull String id) throws Exception { - id = id.replaceFirst("local-", ""); - HttpRequest request = HttpRequest.newBuilder().GET().setHeader("User-Agent", "ModManager " + ModManager.getVersion()) - .uri(URI.create(this.baseUrl + "/api/v1/mod/" + id)).build(); - HttpResponse response = this.http.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() != 200) { - throw new Exception(response.body()); - } - return gson.fromJson(response.body(), Mod.class).toDetailedMod(); - } - - @Override - public List getVersionsForMod(String id) throws Exception { - HttpRequest request = HttpRequest.newBuilder().GET().setHeader("User-Agent", "ModManager " + ModManager.getVersion()) - .uri(URI.create(this.baseUrl + "/api/v1/mod/" + id + "/version")).build(); - HttpResponse response = this.http.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() != 200) { - throw new Exception(response.body()); - } - List versions = gson.fromJson(response.body(), new TypeToken>() { - }.getType()); - versions = versions.stream().filter(v -> v.getLoaders().contains("fabric")).collect(Collectors.toList()); - return Version.toModVersion(versions); - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/Mod.java b/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/Mod.java deleted file mode 100644 index 80e3dba..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/Mod.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.providers.modrinth.model; - -import com.google.gson.annotations.SerializedName; -import net.minecraft.text.TranslatableText; -import xyz.deathsgun.modmanager.api.mod.Category; -import xyz.deathsgun.modmanager.api.mod.DetailedMod; - -import java.util.ArrayList; -import java.util.List; - -public class Mod { - - private String id; - private String slug; - private String title; - private String description; - private String body; - private License license; - private int downloads; - private List categories; - @SerializedName("issues_url") - private String issuesUrl; - @SerializedName("icon_url") - private String icon; - @SerializedName("source_url") - private String sourceUrl; - @SerializedName("wiki_url") - private String wikiUrl; - private List versions; - - public DetailedMod toDetailedMod() { - ArrayList categoriesList = new ArrayList<>(); - categories.forEach(s -> categoriesList.add(new Category(s, new TranslatableText("modmanager.category." + s)))); - return new DetailedMod(id.replaceFirst("local-", ""), slug, title, description, body, license.name(), icon, downloads, categoriesList, issuesUrl, sourceUrl, wikiUrl, versions); - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/ModResult.java b/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/ModResult.java deleted file mode 100644 index f1c041f..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/ModResult.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.providers.modrinth.model; - -import com.google.gson.annotations.SerializedName; - -import java.util.List; - -public class ModResult { - @SerializedName("mod_id") - private String modId; - private String slug; - private String author; - private String title; - private String description; - private List versions; - @SerializedName("icon_url") - private String iconUrl; - - public String getModId() { - return modId; - } - - public String getTitle() { - return title; - } - - public String getDescription() { - return description; - } - - public List getVersions() { - return versions; - } - - public String getIconUrl() { - return iconUrl; - } - - public String getSlug() { - return slug; - } - - public String getAuthor() { - return author; - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/SearchResponse.java b/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/SearchResponse.java deleted file mode 100644 index 938ae20..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/SearchResponse.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.providers.modrinth.model; - -import com.google.gson.annotations.SerializedName; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -public class SearchResponse { - - int limit; - @SerializedName("total_hits") - int totalHits; - private List hits; - private int offset; - - public ArrayList toSummarizedMods() { - ArrayList result = new ArrayList<>(); - for (ModResult modResult : hits) { - String slug = modResult.getSlug().toLowerCase(Locale.ROOT).replaceAll(" ", ""); - result.add(new SummarizedMod(modResult.getModId().replaceAll("local-", ""), slug, modResult.getAuthor(), modResult.getTitle(), modResult.getVersions(), modResult.getDescription(), modResult.getIconUrl())); - } - return result; - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/Version.java b/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/Version.java deleted file mode 100644 index 5de26b5..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/Version.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.providers.modrinth.model; - -import com.google.gson.annotations.SerializedName; -import xyz.deathsgun.modmanager.api.mod.Asset; -import xyz.deathsgun.modmanager.api.mod.ModVersion; -import xyz.deathsgun.modmanager.api.mod.VersionType; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -public class Version { - - @SerializedName("version_number") - private String version; - private String changelog; - @SerializedName("date_published") - private Date releaseDate; - @SerializedName("version_type") - private String type; - @SerializedName("game_versions") - private List gameVersions; - private List files; - private List loaders; - - public static ArrayList toModVersion(List versions) { - ArrayList result = new ArrayList<>(); - for (Version version : versions) { - result.add(new ModVersion(version.version, version.changelog, VersionType.fromString(version.version), version.releaseDate, version.gameVersions, File.toFiles(version.files))); - } - return result; - } - - public List getLoaders() { - return loaders; - } - - private static class File { - private String url; - private String filename; - - public static List toFiles(List files) { - ArrayList result = new ArrayList<>(); - for (File file : files) { - result.add(new Asset(file.url, file.filename)); - } - return result; - } - } - -} diff --git a/src/main/java/xyz/deathsgun/modmanager/services/ManipulationService.java b/src/main/java/xyz/deathsgun/modmanager/services/ManipulationService.java deleted file mode 100644 index 6029284..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/services/ManipulationService.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.services; - -import xyz.deathsgun.modmanager.api.manipulation.ManipulationTask; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -public class ManipulationService extends Thread { - - private final ArrayList tasks = new ArrayList<>(); - - public ManipulationService() { - setName("ModMenu"); - start(); - } - - public void add(ManipulationTask task) { - this.tasks.add(task); - } - - public void remove(String taskId) { - this.tasks.removeIf(task -> task.getId().equals(taskId)); - } - - @Override - public void run() { - while (isAlive()) { - List scheduledTasks = new ArrayList<>(tasks).stream().filter(task -> task.getState() == ManipulationTask.TaskState.SCHEDULED) - .collect(Collectors.toList()); - for (ManipulationTask task : scheduledTasks) { - task.executeTask(); - } - } - } - - public Optional getTask(String id) { - return tasks.stream().filter(task -> task.getId().equals(id)).findFirst(); - } - - public boolean isState(String taskId, ManipulationTask.TaskState state) { - Optional task = getTask(taskId); - if (task.isEmpty()) { - return false; - } - return task.get().getState() == state; - } - - public void removeTasks(String modId) { - this.tasks.removeIf(task -> task.getSubject().id().equals(modId)); - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/services/UpdateCheckService.java b/src/main/java/xyz/deathsgun/modmanager/services/UpdateCheckService.java deleted file mode 100644 index ee491d0..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/services/UpdateCheckService.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.services; - -import com.terraformersmc.modmenu.util.mod.fabric.CustomValueUtil; -import net.fabricmc.loader.api.FabricLoader; -import net.fabricmc.loader.api.ModContainer; -import net.fabricmc.loader.api.SemanticVersion; -import net.fabricmc.loader.api.metadata.ModMetadata; -import net.fabricmc.loader.util.version.VersionDeserializer; -import net.minecraft.MinecraftVersion; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.Nullable; -import xyz.deathsgun.modmanager.ModManager; -import xyz.deathsgun.modmanager.api.mod.DetailedMod; -import xyz.deathsgun.modmanager.api.mod.ModVersion; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; -import xyz.deathsgun.modmanager.api.mod.VersionType; -import xyz.deathsgun.modmanager.api.provider.IModProvider; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; - -public class UpdateCheckService extends Thread { - - private final Logger logger = LogManager.getLogger("Update Checker"); - private final ArrayList updates = new ArrayList<>(); - private final List blockedIds = Arrays.asList("java", "minecraft"); - - public UpdateCheckService() { - super(); - setName("ModManager"); - } - - @Override - public void run() { - FabricLoader.getInstance().getAllMods().stream().map(ModContainer::getMetadata) - .filter(metadata -> !metadata.getId().startsWith("fabric") && !metadata.containsCustomValue("fabric-api:module-lifecycle")) - .filter(metadata -> !CustomValueUtil.getBoolean("fabric-loom:generated", metadata).orElse(false)) - .filter(metadata -> !blockedIds.contains(metadata.getId())) - .forEach(this::checkForUpdate); - } - - private void checkForUpdate(ModMetadata metadata) { - if (!(metadata.getVersion() instanceof SemanticVersion)) { - LogManager.getLogger().warn("Update checking for mod {} not supported because it has no semantic version scheme", metadata.getId()); - return; - } - try { - String modId = findModId(metadata); - ModVersion version = getUpdateVersion(modId, metadata); - if (version == null) { - return; - } - updates.add(new AvailableUpdates(modId, metadata.getId(), version)); - } catch (Exception e) { - logger.error("Failed to check for updates for {}: {}", metadata.getId(), e.getMessage()); - } - } - - private String findModId(ModMetadata container) throws Exception { - IModProvider provider = ModManager.getModProvider(); - try { - DetailedMod mod = provider.getMod(container.getId()); // Try mod id most of the time it's also the Modrinth slug - return mod.id(); - } catch (Exception ignored) { - } - List hits = provider.getMods(container.getName(), 0, 10); - if (hits.isEmpty()) { - throw new Exception(String.format("Mod %s not found on %s", container.getId(), provider.getName())); - } - for (SummarizedMod hit : hits) { - if (hit.name().equals(container.getName())) { - return hit.id(); - } - } - throw new Exception(String.format("Mod %s not found on %s", container.getId(), provider.getName())); - } - - @Nullable - private ModVersion getUpdateVersion(String modId, ModMetadata container) throws Exception { - IModProvider provider = ModManager.getModProvider(); - SemanticVersion installedVersion = (SemanticVersion) container.getVersion(); - List versions = provider.getVersionsForMod(modId); - ModVersion latest = null; - SemanticVersion latestVersion = null; - for (ModVersion modVersion : versions) { - if (!modVersion.gameVersions().contains(MinecraftVersion.GAME_VERSION.getReleaseTarget()) || modVersion.type() != VersionType.RELEASE) { - continue; - } - SemanticVersion version = VersionDeserializer.deserializeSemantic(modVersion.version()); - if (latestVersion == null || (version.compareTo(installedVersion) > 0 && version.compareTo(latestVersion) > 0)) { - latest = modVersion; - latestVersion = version; - } - } - if (latest == null || installedVersion.compareTo(latestVersion) >= 0) { - logger.info("No update for {} found!", container.getId()); - return null; - } - logger.info("Found an update for {} {} new version: {}", container.getId(), container.getVersion().toString(), latestVersion.toString()); - return latest; - } - - public boolean isUpdateAvailable(SummarizedMod mod, ModMetadata modContainer) { - return this.updates.stream().anyMatch(update -> update.modId.equalsIgnoreCase(mod.id()) || update.fabricModId.equalsIgnoreCase(modContainer.getId())); - } - - public boolean updatesAvailable() { - return !this.updates.isEmpty(); - } - - public void removeUpdate(String modId) { - this.updates.removeIf(update -> Objects.equals(update.modId, modId)); - } - - @Nullable - public ModVersion getUpdate(SummarizedMod mod) { - for (AvailableUpdates update : this.updates) { - if (update.modId.equalsIgnoreCase(mod.id())) { - return update.update; - } - } - return null; - } - - public int updatesAvailableCount() { - return this.updates.size(); - } - - public List getUpdatableMods() { - ArrayList mods = new ArrayList<>(); - for (AvailableUpdates update : this.updates) { - try { - mods.add(ModManager.getModProvider().getMod(update.modId())); - } catch (Exception e) { - e.printStackTrace(); - } - } - return mods; - } - - private record AvailableUpdates(String modId, String fabricModId, - ModVersion update) { - } - -} diff --git a/src/main/java/xyz/deathsgun/modmanager/tasks/IconDownloadTask.java b/src/main/java/xyz/deathsgun/modmanager/tasks/IconDownloadTask.java deleted file mode 100644 index 21d348e..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/tasks/IconDownloadTask.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.tasks; - -import net.minecraft.client.texture.NativeImage; -import net.minecraft.util.Identifier; -import org.apache.logging.log4j.LogManager; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import xyz.deathsgun.modmanager.ModManager; -import xyz.deathsgun.modmanager.api.manipulation.NetworkTask; -import xyz.deathsgun.modmanager.api.manipulation.TaskCallback; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; - -import java.io.InputStream; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.Locale; - -public class IconDownloadTask extends NetworkTask { - - private final HttpClient http = HttpClient.newHttpClient(); - - public IconDownloadTask(@NotNull String id, @NotNull SummarizedMod subject, @Nullable TaskCallback taskCallback) { - super(id, subject, taskCallback); - logger = LogManager.getLogger("Icon downloader"); - } - - @Override - protected void execute() throws Exception { - if (subject == null) { - throw new Exception("Summarized mod is null"); - } - debug("Downloading icon for {}", subject.slug()); - debug("Using URL {}", subject.icon()); - HttpRequest request = build(HttpRequest.newBuilder().GET().uri(URI.create(subject.icon()))); - HttpResponse response = this.http.send(request, HttpResponse.BodyHandlers.ofInputStream()); - if (response.statusCode() != 200) { - logger.error("Failed to download icon for {} ! received status code: {}", subject.slug(), response.statusCode()); - throw new Exception(String.format("Received invalid status code: %d", response.statusCode())); - } - debug("Reading icon for {}", subject.slug()); - NativeImage image = NativeImage.read(response.body()); - Identifier iconLocation = new Identifier("modmanager", subject.id().toLowerCase(Locale.ROOT) + "_icon"); - ModManager.getIconManager().registerIcon(subject.id(), iconLocation, image); - debug("Finished downloading icon for {}", subject.slug()); - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/tasks/ModDownloadTask.java b/src/main/java/xyz/deathsgun/modmanager/tasks/ModDownloadTask.java deleted file mode 100644 index 4917231..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/tasks/ModDownloadTask.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.tasks; - -import org.apache.logging.log4j.LogManager; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import xyz.deathsgun.modmanager.ModManager; -import xyz.deathsgun.modmanager.api.manipulation.NetworkTask; -import xyz.deathsgun.modmanager.api.manipulation.TaskCallback; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; -import xyz.deathsgun.modmanager.util.InstallationUtil; - -public class ModDownloadTask extends NetworkTask { - - public ModDownloadTask(@NotNull String id, @NotNull SummarizedMod subject, @Nullable TaskCallback taskCallback) { - super(id, subject, taskCallback); - logger = LogManager.getLogger("Mod downloader"); - } - - @Override - protected void execute() throws Exception { - if (subject == null) { - throw new Exception("Summarized mod is empty"); - } - InstallationUtil.downloadMod(http, InstallationUtil.getInstallableVersionForMod(subject)); - ModManager.getModManipulationManager().markManuallyInstalled(subject); - } - -} diff --git a/src/main/java/xyz/deathsgun/modmanager/tasks/ModRemovalTask.java b/src/main/java/xyz/deathsgun/modmanager/tasks/ModRemovalTask.java deleted file mode 100644 index bc0a436..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/tasks/ModRemovalTask.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.tasks; - -import net.fabricmc.loader.api.ModContainer; -import org.apache.logging.log4j.LogManager; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import xyz.deathsgun.modmanager.ModManager; -import xyz.deathsgun.modmanager.api.manipulation.ManipulationTask; -import xyz.deathsgun.modmanager.api.manipulation.TaskCallback; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; -import xyz.deathsgun.modmanager.util.FabricMods; - -import java.nio.file.Path; -import java.util.Optional; - -public class ModRemovalTask extends ManipulationTask { - - public ModRemovalTask(@NotNull String taskId, @NotNull SummarizedMod subject, @Nullable TaskCallback taskCallback) { - super(taskId, subject, taskCallback); - logger = LogManager.getLogger("Mod remover"); - } - - @Override - protected void execute() throws Exception { - Optional container = FabricMods.getModContainerByMod(subject); - String id, name; - if (container.isPresent()) { - id = container.get().getMetadata().getId(); - name = container.get().getMetadata().getName(); - } else { - debug("Getting mod id from summarized mod (this may fail)"); - id = subject.slug(); - name = subject.name(); - } - Path jar = FabricMods.getJarFromModContainer(id, name); - if (jar == null) { - throw new Exception(String.format("Couldn't find jar for %s", subject.name())); - } - //TODO: Delete file directly - jar.toFile().deleteOnExit(); - ModManager.getModManipulationManager().removeManuallyInstalled(subject); - } - -} diff --git a/src/main/java/xyz/deathsgun/modmanager/tasks/ModUpdateTask.java b/src/main/java/xyz/deathsgun/modmanager/tasks/ModUpdateTask.java deleted file mode 100644 index dd5d4ad..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/tasks/ModUpdateTask.java +++ /dev/null @@ -1,48 +0,0 @@ -package xyz.deathsgun.modmanager.tasks; - -import net.fabricmc.loader.api.ModContainer; -import org.apache.logging.log4j.LogManager; -import xyz.deathsgun.modmanager.ModManager; -import xyz.deathsgun.modmanager.api.manipulation.NetworkTask; -import xyz.deathsgun.modmanager.api.manipulation.TaskCallback; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; -import xyz.deathsgun.modmanager.util.FabricMods; -import xyz.deathsgun.modmanager.util.InstallationUtil; - -import java.nio.file.Path; -import java.util.Optional; - -public class ModUpdateTask extends NetworkTask { - - public ModUpdateTask(String taskId, SummarizedMod mod, TaskCallback taskCallback) { - super(taskId, mod, taskCallback); - logger = LogManager.getLogger("Mod remover"); - } - - @SuppressWarnings("DuplicatedCode") - @Override - protected void execute() throws Exception { - if (subject == null) { - throw new Exception("Summarized mod is empty"); - } - Optional container = FabricMods.getModContainerByMod(subject); - String id, name; - if (container.isPresent()) { - id = container.get().getMetadata().getId(); - name = container.get().getMetadata().getName(); - } else { - debug("Getting mod id from summarized mod (this may fail)"); - id = subject.slug(); - name = subject.name(); - } - Path jar = FabricMods.getJarFromModContainer(id, name); - if (jar == null) { - throw new Exception(String.format("Couldn't find jar for %s", subject.name())); - } - //TODO: Delete file directly - jar.toFile().deleteOnExit(); - InstallationUtil.downloadMod(http, InstallationUtil.getInstallableVersionForMod(subject)); - ModManager.getModManipulationManager().markManuallyUpdated(subject); - ModManager.getUpdateChecker().removeUpdate(subject.id()); - } -} diff --git a/src/main/java/xyz/deathsgun/modmanager/util/FabricMods.java b/src/main/java/xyz/deathsgun/modmanager/util/FabricMods.java deleted file mode 100644 index 69c3866..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/util/FabricMods.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.util; - -import com.google.gson.Gson; -import net.fabricmc.loader.api.FabricLoader; -import net.fabricmc.loader.api.ModContainer; -import org.apache.logging.log4j.LogManager; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; -import xyz.deathsgun.modmanager.model.ReducedModMetadata; - -import java.io.InputStreamReader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -public class FabricMods { - - public static Optional getModContainerByMod(SummarizedMod mod) { - return FabricLoader.getInstance().getAllMods().stream() - .filter(metadata -> metadata.getMetadata().getId().equalsIgnoreCase(mod.slug()) || - metadata.getMetadata().getName().equalsIgnoreCase(mod.name())).findFirst(); - } - - public static Path getJarFromModContainer(String id, String name) throws Exception { - List jars = Files.list(FabricLoader.getInstance().getGameDir().resolve("mods")) - .filter(file -> file.toFile().getName().endsWith(".jar")) - .collect(Collectors.toList()); - Gson gson = new Gson(); - for (Path path : jars) { - ZipFile zipFile = new ZipFile(path.toFile()); - ZipEntry entry = zipFile.getEntry("fabric.mod.json"); - if (entry == null) { - LogManager.getLogger().warn("No fabric.mod.json found in {}", path); - continue; - } - ReducedModMetadata metadata = gson.fromJson(new InputStreamReader(zipFile.getInputStream(entry)), ReducedModMetadata.class); - zipFile.close(); - if (metadata.getId().equals(id) || metadata.getName().equals(name)) { - return path; - } - } - return null; - } - -} diff --git a/src/main/java/xyz/deathsgun/modmanager/util/InstallationUtil.java b/src/main/java/xyz/deathsgun/modmanager/util/InstallationUtil.java deleted file mode 100644 index ede0c11..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/util/InstallationUtil.java +++ /dev/null @@ -1,61 +0,0 @@ -package xyz.deathsgun.modmanager.util; - -import net.fabricmc.loader.api.FabricLoader; -import net.fabricmc.loader.api.SemanticVersion; -import net.fabricmc.loader.util.version.VersionDeserializer; -import net.minecraft.MinecraftVersion; -import xyz.deathsgun.modmanager.ModManager; -import xyz.deathsgun.modmanager.api.mod.Asset; -import xyz.deathsgun.modmanager.api.mod.ModVersion; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; -import xyz.deathsgun.modmanager.api.mod.VersionType; -import xyz.deathsgun.modmanager.api.provider.IModProvider; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.file.Path; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -public class InstallationUtil { - - public static ModVersion getInstallableVersionForMod(SummarizedMod mod) throws Exception { - IModProvider provider = ModManager.getModProvider(); - List versions = provider.getVersionsForMod(mod.id()).stream() - .filter(value -> value.gameVersions().contains(MinecraftVersion.GAME_VERSION.getReleaseTarget())) - .filter(value -> value.type() == VersionType.RELEASE).collect(Collectors.toList()); - ModVersion latest = null; - SemanticVersion latestVersion = null; - for (ModVersion modVersion : versions) { - if (!modVersion.gameVersions().contains(MinecraftVersion.GAME_VERSION.getReleaseTarget())) { - continue; - } - SemanticVersion version = VersionDeserializer.deserializeSemantic(modVersion.version()); - if (latestVersion == null || version.compareTo(latestVersion) > 0) { - latest = modVersion; - latestVersion = version; - } - } - return latest; - } - - public static void downloadMod(HttpClient http, ModVersion version) throws Exception { - if (version == null) { - throw new Exception("no version found!"); - } - Optional asset = version.assets().stream().filter(value -> value.filename().endsWith(".jar")).findFirst(); - if (asset.isEmpty()) { - throw new Exception("jar in downloadable assets found"); - } - HttpRequest request = HttpRequest.newBuilder().GET().uri(URI.create(asset.get().url())).build(); - Path output = FabricLoader.getInstance().getGameDir().resolve("mods").resolve(asset.get().filename()); - HttpResponse response = http.send(request, HttpResponse.BodyHandlers.ofFile(output)); - if (response.statusCode() != 200) { - throw new Exception("Invalid status code: " + response.statusCode()); - } - } - -} diff --git a/src/main/java/xyz/deathsgun/modmanager/util/MarkdownPreprocessor.java b/src/main/java/xyz/deathsgun/modmanager/util/MarkdownPreprocessor.java deleted file mode 100644 index 121a677..0000000 --- a/src/main/java/xyz/deathsgun/modmanager/util/MarkdownPreprocessor.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager.util; - -import net.minecraft.text.ClickEvent; -import net.minecraft.text.LiteralText; -import net.minecraft.text.MutableText; -import net.minecraft.util.Formatting; - -import java.util.ArrayList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class MarkdownPreprocessor { - - private static final Pattern BOLD_PATTERN = Pattern.compile("\\*\\*(.*?)\\*\\*"); - private static final Pattern LINK_PATTERN = Pattern.compile("\\[(.*?)]\\((.*?)\\)"); - private static final Pattern IMAGE_PATTERN = Pattern.compile("!\\[(.*?)]\\((.*?)\\)"); - - public static MutableText[] processText(String text) { - text = text.replaceAll("\r", ""); - text = text.replaceAll("
", "\n").replaceAll("
", "\n"); - String[] lines = text.split("\n"); - ArrayList texts = new ArrayList<>(); - for (String line : lines) { - if (IMAGE_PATTERN.matcher(line).find()) { - continue; - } - texts.add(processLine(line)); - } - return texts.toArray(new MutableText[]{}); - } - - private static MutableText processLine(String text) { - if (BOLD_PATTERN.matcher(text).find()) { - return extractBoldText(text); - } - if (LINK_PATTERN.matcher(text).find()) { - return extractLinkText(text); - } - return new LiteralText(text); - } - - private static MutableText extractLinkText(String text) { - Matcher matcher = LINK_PATTERN.matcher(text); - if (!matcher.find()) { - return new LiteralText(text); - } - MutableText linkText = new LiteralText(matcher.group(1)).formatted(Formatting.UNDERLINE, Formatting.BLUE); - return linkText.setStyle(linkText.getStyle().withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, matcher.group(2)))); - } - - private static MutableText extractBoldText(String text) { - Matcher matcher = BOLD_PATTERN.matcher(text); - if (!matcher.find()) { - return new LiteralText(text); - } - String boldText = matcher.group(1); - int begin = text.indexOf(boldText); - LiteralText preText = new LiteralText(text.substring(0, begin).replaceAll("\\*\\*", "")); - MutableText matchedText = new LiteralText(boldText).formatted(Formatting.BOLD); - return preText.append(matchedText).append(extractBoldText(text.substring(begin + 2 + boldText.length()))); - } - -} diff --git a/src/main/java/xyz/deathsgun/modmanager/api/mod/Category.java b/src/main/kotlin/xyz/deathsgun/modmanager/Initializer.kt similarity index 78% rename from src/main/java/xyz/deathsgun/modmanager/api/mod/Category.java rename to src/main/kotlin/xyz/deathsgun/modmanager/Initializer.kt index 3a18aaf..6578b37 100644 --- a/src/main/java/xyz/deathsgun/modmanager/api/mod/Category.java +++ b/src/main/kotlin/xyz/deathsgun/modmanager/Initializer.kt @@ -14,12 +14,13 @@ * limitations under the License. */ -package xyz.deathsgun.modmanager.api.mod; +package xyz.deathsgun.modmanager -import net.minecraft.text.TranslatableText; +import net.fabricmc.api.ModInitializer -public record Category( - String id, - TranslatableText text -) { -} +class Initializer : ModInitializer { + override fun onInitialize() { + + } + +} \ No newline at end of file diff --git a/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/License.java b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt similarity index 74% rename from src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/License.java rename to src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt index f344089..80f73a5 100644 --- a/src/main/java/xyz/deathsgun/modmanager/providers/modrinth/model/License.java +++ b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt @@ -14,15 +14,13 @@ * limitations under the License. */ -package xyz.deathsgun.modmanager.providers.modrinth.model; +package xyz.deathsgun.modmanager -public class License { - private String id; - private String name; - private String url; +class ModManager { - public String name() { - return name; + companion object { + fun getVersion(): String { + return "1.0.0" + } } - -} +} \ No newline at end of file diff --git a/src/main/java/xyz/deathsgun/modmanager/gui/widget/better/IListScreen.java b/src/main/kotlin/xyz/deathsgun/modmanager/api/http/CategoriesResult.kt similarity index 63% rename from src/main/java/xyz/deathsgun/modmanager/gui/widget/better/IListScreen.java rename to src/main/kotlin/xyz/deathsgun/modmanager/api/http/CategoriesResult.kt index 5b1d966..af1f3d1 100644 --- a/src/main/java/xyz/deathsgun/modmanager/gui/widget/better/IListScreen.java +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/http/CategoriesResult.kt @@ -14,16 +14,15 @@ * limitations under the License. */ -package xyz.deathsgun.modmanager.gui.widget.better; +package xyz.deathsgun.modmanager.api.http -import net.minecraft.client.gui.Element; +import net.minecraft.text.TranslatableText +import xyz.deathsgun.modmanager.api.mod.Category -public interface IListScreen { +sealed class CategoriesResult { - Element getFocused(); + data class Success(val categories: List) : CategoriesResult() - void updateSelectedEntry(Object widget, E entry); - - E getEntry(Object widget); + data class Error(val text: TranslatableText, val cause: Exception? = null) : CategoriesResult() } diff --git a/src/main/java/xyz/deathsgun/modmanager/api/mod/VersionType.java b/src/main/kotlin/xyz/deathsgun/modmanager/api/http/ModResult.kt similarity index 66% rename from src/main/java/xyz/deathsgun/modmanager/api/mod/VersionType.java rename to src/main/kotlin/xyz/deathsgun/modmanager/api/http/ModResult.kt index 042ab72..02bd257 100644 --- a/src/main/java/xyz/deathsgun/modmanager/api/mod/VersionType.java +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/http/ModResult.kt @@ -14,16 +14,12 @@ * limitations under the License. */ -package xyz.deathsgun.modmanager.api.mod; +package xyz.deathsgun.modmanager.api.http -public enum VersionType { - RELEASE, BETA; - - public static VersionType fromString(String type) { - return switch (type) { - case "beta" -> BETA; - default -> RELEASE; - }; - } +import net.minecraft.text.TranslatableText +import xyz.deathsgun.modmanager.api.mod.Mod +sealed class ModResult { + data class Success(val mod: Mod) : ModResult() + data class Error(val text: TranslatableText, val cause: Exception? = null) : ModResult() } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/http/ModsResult.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/http/ModsResult.kt new file mode 100644 index 0000000..3a7cbb3 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/http/ModsResult.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.api.http + +import net.minecraft.text.TranslatableText +import xyz.deathsgun.modmanager.api.mod.Mod + +sealed class ModsResult { + data class Success(val mods: List) : ModsResult() + data class Error(val text: TranslatableText, val cause: Exception? = null) : ModsResult() +} diff --git a/src/main/java/xyz/deathsgun/modmanager/api/provider/Sorting.java b/src/main/kotlin/xyz/deathsgun/modmanager/api/http/VersionResult.kt similarity index 64% rename from src/main/java/xyz/deathsgun/modmanager/api/provider/Sorting.java rename to src/main/kotlin/xyz/deathsgun/modmanager/api/http/VersionResult.kt index 168f002..7450b65 100644 --- a/src/main/java/xyz/deathsgun/modmanager/api/provider/Sorting.java +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/http/VersionResult.kt @@ -14,16 +14,14 @@ * limitations under the License. */ -package xyz.deathsgun.modmanager.api.provider; +package xyz.deathsgun.modmanager.api.http -import java.util.Locale; +import net.minecraft.text.TranslatableText +import xyz.deathsgun.modmanager.api.mod.Version -public enum Sorting { +sealed class VersionResult { - RELEVANCE, DOWNLOADS, UPDATED, NEWEST; - - public String toLowerCase() { - return name().toLowerCase(Locale.ROOT); - } + data class Success(val versions: List) : VersionResult() + data class Error(val text: TranslatableText, val cause: Exception? = null) : VersionResult() } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Asset.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Asset.kt new file mode 100644 index 0000000..555a823 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Asset.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.api.mod + +data class Asset( + val url: String, + val filename: String, + val hashes: Map +) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Category.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Category.kt new file mode 100644 index 0000000..94aafdf --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Category.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.api.mod + +import net.minecraft.text.TranslatableText + +data class Category( + val id: String, + val text: TranslatableText +) + diff --git a/src/main/java/xyz/deathsgun/modmanager/api/mod/ModVersion.java b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt similarity index 66% rename from src/main/java/xyz/deathsgun/modmanager/api/mod/ModVersion.java rename to src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt index f8b3686..301d3a6 100644 --- a/src/main/java/xyz/deathsgun/modmanager/api/mod/ModVersion.java +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt @@ -14,17 +14,16 @@ * limitations under the License. */ -package xyz.deathsgun.modmanager.api.mod; +package xyz.deathsgun.modmanager.api.mod -import java.util.Date; -import java.util.List; - -public record ModVersion( - String version, - String changelog, - VersionType type, - Date releaseDate, - List gameVersions, - List assets -) { -} +data class Mod( + val id: String, + val slug: String, + val author: String, + val name: String, + val shortDescription: String, + val iconUrl: String, + val description: String?, + val license: String?, + val categories: List, +) diff --git a/src/main/java/xyz/deathsgun/modmanager/api/mod/ModState.java b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/State.kt similarity index 84% rename from src/main/java/xyz/deathsgun/modmanager/api/mod/ModState.java rename to src/main/kotlin/xyz/deathsgun/modmanager/api/mod/State.kt index 389675b..b019955 100644 --- a/src/main/java/xyz/deathsgun/modmanager/api/mod/ModState.java +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/State.kt @@ -14,24 +14,31 @@ * limitations under the License. */ -package xyz.deathsgun.modmanager.api.mod; +package xyz.deathsgun.modmanager.api.mod -public enum ModState { +enum class State { /** * Returns this if the mod has been found in the current * mod list */ INSTALLED, + /** * Returns this if the mod has been found and also has been * checked for updates */ OUTDATED, + + /** + * Returns this if the mod has been found but been removed by ModManager + */ + REMOVED, + /** * Returns this if the mod was not found */ DOWNLOADABLE, CHECKING -} +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Version.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Version.kt new file mode 100644 index 0000000..2009bd9 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Version.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.api.mod + +data class Version( + val version: String, + val changelog: String, + val type: VersionType, + val gameVersions: List, + val assets: List +) diff --git a/src/main/java/xyz/deathsgun/modmanager/api/mod/Asset.java b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/VersionType.kt similarity index 84% rename from src/main/java/xyz/deathsgun/modmanager/api/mod/Asset.java rename to src/main/kotlin/xyz/deathsgun/modmanager/api/mod/VersionType.kt index c9d18f2..51b94dc 100644 --- a/src/main/java/xyz/deathsgun/modmanager/api/mod/Asset.java +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/VersionType.kt @@ -14,10 +14,8 @@ * limitations under the License. */ -package xyz.deathsgun.modmanager.api.mod; +package xyz.deathsgun.modmanager.api.mod -public record Asset( - String url, - String filename -) { -} +enum class VersionType { + ALPHA, BETA, RELEASE, UNKNOWN +} \ No newline at end of file diff --git a/src/main/java/xyz/deathsgun/modmanager/api/provider/IModProvider.java b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt similarity index 56% rename from src/main/java/xyz/deathsgun/modmanager/api/provider/IModProvider.java rename to src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt index 7327d0e..8e0526c 100644 --- a/src/main/java/xyz/deathsgun/modmanager/api/provider/IModProvider.java +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt @@ -14,39 +14,33 @@ * limitations under the License. */ -package xyz.deathsgun.modmanager.api.provider; +package xyz.deathsgun.modmanager.api.provider -import xyz.deathsgun.modmanager.api.mod.Category; -import xyz.deathsgun.modmanager.api.mod.DetailedMod; -import xyz.deathsgun.modmanager.api.mod.ModVersion; -import xyz.deathsgun.modmanager.api.mod.SummarizedMod; +import xyz.deathsgun.modmanager.api.http.CategoriesResult +import xyz.deathsgun.modmanager.api.http.ModResult +import xyz.deathsgun.modmanager.api.http.ModsResult +import xyz.deathsgun.modmanager.api.mod.Category +import xyz.deathsgun.modmanager.api.mod.Mod -import java.util.List; - -/** - * Provides mods to the ModMenu graphical interface - * - * @since 1.0.0 - */ -public interface IModProvider { +interface IModProvider { /** * Name of the provider. This will be shown * in the GUI * - * @return returns a user friendly name of the mod provider implementation + * @return returns a user-friendly name of the mod provider implementation */ - String getName(); + fun getName(): String /** * Returns a list of all possible mod categories also with an translatable text * * @return a list of all mod categories */ - List getCategories() throws Exception; + fun getCategories(): CategoriesResult /** - * Returns a limited number of {@link SummarizedMod}'s sorted + * Returns a limited number of [Mod]'s sorted * in a given way. * * @param sorting the requested sorting of the mods @@ -54,37 +48,33 @@ public interface IModProvider { * @param limit to not overfill the ui and for shorter loading times the amount of returned mods needs to limited * @return a list of sorted mods */ - List getMods(Sorting sorting, int page, int limit) throws Exception; + fun getMods(sorting: Sorting, page: Int, limit: Int): ModsResult /** - * Returns a limited number of {@link SummarizedMod}'s from the specified category + * Returns a limited number of [Mod]'s from the specified category * * @param category the category of all the mods * @param page the requested from the UI starting at 1 * @param limit to not overfill the ui and for shorter loading times the amount of returned mods needs to limited * @return a list of sorted mods */ - List getMods(Category category, int page, int limit) throws Exception; + fun getMods(category: Category, page: Int, limit: Int): ModsResult /** - * Returns a limited number of {@link SummarizedMod}'s from a given search. + * Returns a limited number of [Mod]'s from a given search. * * @param query the search string * @param page the current requested page starts at 0 * @param limit the amount of mods to return * @return a list of mods matching the search term */ - List getMods(String query, int page, int limit) throws Exception; + fun getMods(query: String, page: Int, limit: Int): ModsResult /** - * Returns a more detailed representation of the mod de + * Returns a more detailed representation of the mod * - * @param id the id from {@link SummarizedMod#id()} which is used to - * @return a detailed representation of {@link SummarizedMod} + * @param mod the [Mod] which is used to receive + * @return a more detailed representation of [Mod] */ - DetailedMod getMod(String id) throws Exception; - - List getVersionsForMod(String id) throws Exception; - - -} + fun getMod(mod: Mod): ModResult +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModUpdateProvider.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModUpdateProvider.kt new file mode 100644 index 0000000..6796e0d --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModUpdateProvider.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.api.provider + +import xyz.deathsgun.modmanager.api.http.VersionResult + +interface IModUpdateProvider { + + fun getVersionsForMod(id: String): VersionResult + +} \ No newline at end of file diff --git a/src/main/java/xyz/deathsgun/modmanager/api/manipulation/TaskCallback.java b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/Sorting.kt similarity index 81% rename from src/main/java/xyz/deathsgun/modmanager/api/manipulation/TaskCallback.java rename to src/main/kotlin/xyz/deathsgun/modmanager/api/provider/Sorting.kt index d092d2e..06acc90 100644 --- a/src/main/java/xyz/deathsgun/modmanager/api/manipulation/TaskCallback.java +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/Sorting.kt @@ -13,11 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package xyz.deathsgun.modmanager.api.provider -package xyz.deathsgun.modmanager.api.manipulation; - -public interface TaskCallback { - - void onTaskFinish(ManipulationTask task); - -} +enum class Sorting { + RELEVANCE, DOWNLOADS, UPDATED, NEWEST; +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt b/src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt new file mode 100644 index 0000000..db2a634 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.config + +import xyz.deathsgun.modmanager.api.mod.VersionType + +data class Config( + var provider: String, + var updateChannel: VersionType +) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt new file mode 100644 index 0000000..44c6ab5 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt @@ -0,0 +1,245 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.providers.modrinth + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import net.minecraft.client.MinecraftClient +import net.minecraft.text.TranslatableText +import org.apache.http.client.utils.URIBuilder +import org.apache.logging.log4j.LogManager +import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.api.http.CategoriesResult +import xyz.deathsgun.modmanager.api.http.ModResult +import xyz.deathsgun.modmanager.api.http.ModsResult +import xyz.deathsgun.modmanager.api.http.VersionResult +import xyz.deathsgun.modmanager.api.mod.* +import xyz.deathsgun.modmanager.api.provider.IModProvider +import xyz.deathsgun.modmanager.api.provider.IModUpdateProvider +import xyz.deathsgun.modmanager.api.provider.Sorting +import xyz.deathsgun.modmanager.providers.modrinth.models.DetailedMod +import xyz.deathsgun.modmanager.providers.modrinth.models.ModrinthVersion +import xyz.deathsgun.modmanager.providers.modrinth.models.SearchResult +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse + +@OptIn(ExperimentalSerializationApi::class) +class Modrinth : IModProvider, IModUpdateProvider { + + private val logger = LogManager.getLogger("Modrinth") + private val categories: ArrayList = ArrayList() + private val cache: HashMap = HashMap() + private val baseUri = "https://api.modrinth.com" + private val http = HttpClient.newHttpClient() + + + override fun getName(): String { + return "Modrinth" + } + + override fun getCategories(): CategoriesResult { + if (this.categories.isNotEmpty()) { + return CategoriesResult.Success(this.categories) + } + logger.info("Getting categories") + val request = HttpRequest.newBuilder().GET().setHeader("User-Agent", "ModManager " + ModManager.getVersion()) + .uri(URI.create("${baseUri}/api/v1/tag/category")).build() + val response = this.http.send(request, HttpResponse.BodyHandlers.ofString()) + return try { + val categories = json.decodeFromString>(response.body()) + for (category in categories) { + this.categories.add( + Category( + category, + TranslatableText(String.format("modmanager.category.%s", category)) + ) + ) + } + CategoriesResult.Success(this.categories) + } catch (e: Exception) { + logger.error("Error while getting categories: ", e) + CategoriesResult.Error(TranslatableText("modmanager.response.failedToParse"), e) + } + } + + override fun getMods(sorting: Sorting, page: Int, limit: Int): ModsResult { + logger.info("Getting a general list of mods") + val builder = URIBuilder("${baseUri}/api/v1/mod") + builder.addParameter("index", sorting.name.lowercase()) + builder.addParameter("filters", "categories=\"fabric\"") + return getMods(builder, page, limit) + } + + override fun getMods(category: Category, page: Int, limit: Int): ModsResult { + logger.info("Getting category '{}' from Modrinth", category.id) + val key = java.lang.String.format("%s|%d|%d", category.id, page, limit) + if (this.cache.containsKey(key)) { + val cachedResult = this.cache[key]!! + if (cachedResult is ModsResult.Success) { + return cachedResult + } + } + val builder = URIBuilder("${baseUri}/api/v1/mod") + builder.addParameter( + "filters", + String.format( + "categories=\"fabric\" AND NOT client_side=\"unsupported\" AND categories=\"%s\"", + category.id + ) + ) + return try { + val result = getMods(builder, page, limit) + if (result is ModsResult.Success) { + this.cache[key] = result + } + result + } catch (e: Exception) { + ModsResult.Error(TranslatableText("modmanager.error.unknown"), e) + } + } + + override fun getMods(query: String, page: Int, limit: Int): ModsResult { + logger.info("Searching for '{}' in Modrinth", query) + val builder = URIBuilder("${baseUri}/api/v1/mod") + builder.addParameter("query", query) + builder.addParameter("filters", "categories=\"fabric\" AND NOT client_side=\"unsupported\"") + return try { + getMods(builder, page, limit) + } catch (e: Exception) { + ModsResult.Error(TranslatableText("modmanager.error.unknown"), e) + } + } + + private fun getMods(builder: URIBuilder, page: Int, limit: Int): ModsResult { + builder.addParameter( + "version", + String.format("versions=%s", MinecraftClient.getInstance()?.game?.version?.releaseTarget ?: "1.17.1") + ) + builder.addParameter("offset", (page * limit).toString()) + builder.addParameter("limit", limit.toString()) + val request = HttpRequest.newBuilder().GET().setHeader("User-Agent", "ModManager " + ModManager.getVersion()) + .uri(builder.build()).build() + val response = this.http.send(request, HttpResponse.BodyHandlers.ofString()) + if (response.statusCode() != 200) { + return ModsResult.Error(TranslatableText("modmanager.error.invalidStatus")) + } + return try { + val result = json.decodeFromString(response.body()) + ModsResult.Success(result.toList()) + } catch (e: Exception) { + logger.error("Error while requesting mods", e) + ModsResult.Error(TranslatableText("modmanager.error.failedToParse")) + } + } + + private val json = Json { + this.ignoreUnknownKeys = true + } + + override fun getMod(mod: Mod): ModResult { + val id = mod.id.replaceFirst("local-", "") + val request = HttpRequest.newBuilder().GET() + .setHeader("User-Agent", "ModManager " + ModManager.getVersion()) + .uri(URI.create("${baseUri}/api/v1/mod/${id}")).build() + val response = try { + this.http.send(request, HttpResponse.BodyHandlers.ofString()) + } catch (e: Exception) { + logger.error("Error while getting mod", e) + return ModResult.Error(TranslatableText("modmanager.error.network"), e) + } + if (response.statusCode() != 200) { + return ModResult.Error(TranslatableText("modmanager.error.invalidStatus")) + } + return try { + val result = json.decodeFromString(response.body()) + val categoriesList = ArrayList() + result.categories.forEach { categoryId -> + categoriesList.add( + Category( + categoryId, + TranslatableText("modmanager.category.${categoryId}") + ) + ) + } + ModResult.Success( + Mod( + id = result.id, + slug = result.slug, + author = mod.author, + name = result.title, + shortDescription = mod.shortDescription, + iconUrl = mod.iconUrl, + description = result.body, + license = result.license.name, + categories = categoriesList + ) + ) + } catch (e: Exception) { + ModResult.Error(TranslatableText("modmanager.error.failedToParse"), e) + } + } + + override fun getVersionsForMod(id: String): VersionResult { + val request = HttpRequest.newBuilder().GET() + .setHeader("User-Agent", "ModManager " + ModManager.getVersion()) + .uri(URI.create("${baseUri}/api/v1/mod/${id}/version")).build() + val response = try { + this.http.send(request, HttpResponse.BodyHandlers.ofString()) + } catch (e: Exception) { + logger.error("Error while getting mod", e) + return VersionResult.Error(TranslatableText("modmanager.error.network"), e) + } + if (response.statusCode() != 200) { + return VersionResult.Error(TranslatableText("modmanager.error.invalidStatus")) + } + return try { + val modrinthVersions = json.decodeFromString>(response.body()) + val versions = ArrayList() + for (modVersion in modrinthVersions) { + val assets = ArrayList() + for (file in modVersion.files) { + assets.add(Asset(file.url, file.filename, file.hashes)) + } + versions.add( + Version( + modVersion.version, + modVersion.changelog, + getVersionType(modVersion.type), + modVersion.gameVersions, + assets + ) + ) + } + VersionResult.Success(versions) + } catch (e: Exception) { + VersionResult.Error(TranslatableText("modmanager.error.failedToParse"), e) + } + } + + private fun getVersionType(id: String): VersionType { + return when (id) { + "release" -> VersionType.RELEASE + "alpha" -> VersionType.ALPHA + "beta" -> VersionType.BETA + else -> VersionType.UNKNOWN + } + } + +} \ No newline at end of file diff --git a/src/main/java/xyz/deathsgun/modmanager/api/mod/SummarizedMod.java b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/DetailedMod.kt similarity index 61% rename from src/main/java/xyz/deathsgun/modmanager/api/mod/SummarizedMod.java rename to src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/DetailedMod.kt index 24e58a9..6c3abf9 100644 --- a/src/main/java/xyz/deathsgun/modmanager/api/mod/SummarizedMod.java +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/DetailedMod.kt @@ -14,20 +14,20 @@ * limitations under the License. */ -package xyz.deathsgun.modmanager.api.mod; +package xyz.deathsgun.modmanager.providers.modrinth.models -import java.util.List; +import kotlinx.serialization.Serializable -/** - * A short representation for Mods used to - */ -public record SummarizedMod( - String id, - String slug, - String author, - String name, - List latest, - String description, - String icon +@Serializable +data class DetailedMod( + val id: String, + val slug: String, + val title: String, + val description: String, + val body: String, + val license: License, + val categories: List ) { + @Serializable + data class License(val name: String) } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModResult.kt b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModResult.kt new file mode 100644 index 0000000..ed5412a --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModResult.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.providers.modrinth.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ModResult( + @SerialName("mod_id") + val id: String, + val slug: String, + val author: String, + val title: String, + val description: String, + @SerialName("icon_url") + val iconUrl: String, + val categories: ArrayList +) diff --git a/src/main/java/xyz/deathsgun/modmanager/api/mod/DetailedMod.java b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModrinthVersion.kt similarity index 50% rename from src/main/java/xyz/deathsgun/modmanager/api/mod/DetailedMod.java rename to src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModrinthVersion.kt index c7ea80c..09569c8 100644 --- a/src/main/java/xyz/deathsgun/modmanager/api/mod/DetailedMod.java +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModrinthVersion.kt @@ -14,27 +14,27 @@ * limitations under the License. */ -package xyz.deathsgun.modmanager.api.mod; +package xyz.deathsgun.modmanager.providers.modrinth.models -import java.util.Collections; -import java.util.List; +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable -public record DetailedMod( - String id, - String slug, - String name, - String description, - String body, - String license, - String icon, - int downloads, - List categories, - String issuesUrl, - String sourceUrl, - String wikiUrl, - List versions +@Serializable +data class ModrinthVersion( + @SerialName("version_number") + val version: String, + val changelog: String, + @SerialName("version_type") + val type: String, + @SerialName("game_versions") + val gameVersions: List, + val files: List, + val loaders: List, ) { - public SummarizedMod toSummarizedMod() { - return new SummarizedMod(id, slug, "", name, Collections.emptyList(), description, icon); - } + @Serializable + data class File( + val hashes: Map, + val filename: String, + val url: String + ) } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/SearchResult.kt b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/SearchResult.kt new file mode 100644 index 0000000..9f02636 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/SearchResult.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.providers.modrinth.models + +import kotlinx.serialization.Serializable +import net.minecraft.text.TranslatableText +import xyz.deathsgun.modmanager.api.mod.Category +import xyz.deathsgun.modmanager.api.mod.Mod + +@Serializable +data class SearchResult( + val hits: List +) { + fun toList(): List { + val mods = ArrayList() + for (mod in hits) { + val categoriesList = ArrayList() + mod.categories.forEach { categoryId -> + categoriesList.add( + Category( + categoryId, + TranslatableText("modmanager.category.${categoryId}") + ) + ) + } + mods.add( + Mod( + mod.id, + mod.slug, + mod.author, + mod.title, + mod.description, + mod.iconUrl, + null, + null, + categoriesList, + ) + ) + } + return mods + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 9b9288f..66d7b8c 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -17,16 +17,20 @@ "environment": "client", "entrypoints": { "client": [ - "xyz.deathsgun.modmanager.ModManager" - ] + { + "adapter": "kotlin", + "value": "xyz.deathsgun.modmanager.Initializer" + } + ] }, "mixins": [ "modmanager.mixins.json" ], "depends": { - "fabricloader": ">=0.11.3", - "modmenu": ">=${modmenu_version}", - "minecraft": "1.17.x", - "java": ">=16" + "fabricloader": ">=0.11.3", + "modmenu": ">=${modmenu_version}", + "fabric-language-kotlin": ">=${fabric_kotlin_version}", + "minecraft": "1.17.x", + "java": ">=16" } } diff --git a/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt b/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt new file mode 100644 index 0000000..1c1301d --- /dev/null +++ b/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt @@ -0,0 +1,169 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.providers.modrinth + +import net.minecraft.text.TranslatableText +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.fail +import xyz.deathsgun.modmanager.api.http.CategoriesResult +import xyz.deathsgun.modmanager.api.http.ModResult +import xyz.deathsgun.modmanager.api.http.ModsResult +import xyz.deathsgun.modmanager.api.http.VersionResult +import xyz.deathsgun.modmanager.api.mod.Category +import xyz.deathsgun.modmanager.api.mod.Mod +import xyz.deathsgun.modmanager.api.mod.VersionType +import xyz.deathsgun.modmanager.api.provider.Sorting +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +internal class ModrinthTest { + + private val modrinth = Modrinth() + + @Test + fun getCategories() { + val result = modrinth.getCategories() + if (result is CategoriesResult.Error) { + result.cause?.let { + fail(result.text.key, it) + } + fail(result.text.key) + } + val categories = (result as CategoriesResult.Success).categories + assert(categories.isNotEmpty()) + categories.forEach { + assert(it.id.isNotEmpty()) + assertEquals(String.format("modmanager.category.%s", it.id), it.text.key) + } + } + + @Test + fun getModsBySorting() { + val result = modrinth.getMods(Sorting.NEWEST, 0, 10) + if (result is ModsResult.Error) { + result.cause?.let { + fail(result.text.key, it) + } + fail(result.text.key) + } + val mods = (result as ModsResult.Success).mods + checkMods(mods) + } + + @Test + fun getModsByCategory() { + val result = modrinth.getMods(Category("misc", TranslatableText("")), 0, 10) + if (result is ModsResult.Error) { + result.cause?.let { + fail(result.text.key, it) + } + fail(result.text.key) + } + val mods = (result as ModsResult.Success).mods + checkMods(mods) + } + + @Test + fun getModsByQuery() { + val result = modrinth.getMods("Mod", 0, 10) + if (result is ModsResult.Error) { + result.cause?.let { + fail(result.text.key, it) + } + fail(result.text.key) + } + val mods = (result as ModsResult.Success).mods + checkMods(mods) + } + + private fun checkMods(mods: List) { + assert(mods.isNotEmpty()) + assertEquals(mods.size, 10) + mods.forEach { + assert(it.id.isNotEmpty()) + assert(it.slug.isNotEmpty()) + assert(it.name.isNotEmpty()) + assert(it.author.isNotEmpty()) + assert(it.iconUrl.isNotEmpty()) + assert(it.shortDescription.isNotEmpty()) + assert(it.categories.isNotEmpty()) + + // Only filled when getMod(id) is called + assertEquals(null, it.description, "description should be null as it's only loaded by getMod") + assertEquals(null, it.license, "description should be null as it's only loaded by getMod") + } + } + + @Test + fun getMod() { + val testMod = modrinth.getMods(Sorting.NEWEST, 0, 1) + if (testMod is ModsResult.Error) { + testMod.cause?.let { + fail(testMod.text.key, it) + } + fail(testMod.text.key) + } + val result = modrinth.getMod((testMod as ModsResult.Success).mods[0]) + if (result is ModResult.Error) { + result.cause?.let { + fail(result.text.key, it) + } + fail(result.text.key) + } + val mod = (result as ModResult.Success).mod + assert(mod.id.isNotEmpty()) + assert(mod.slug.isNotEmpty()) + assert(mod.name.isNotEmpty()) + assert(mod.author.isNotEmpty()) + assert(mod.iconUrl.isNotEmpty()) + assert(mod.shortDescription.isNotEmpty()) + assert(mod.categories.isNotEmpty()) + assertNotEquals(mod.description, null) + assert(mod.description!!.isNotEmpty()) + assertNotEquals(mod.license, null) + assert(mod.license!!.isNotEmpty()) + } + + @Test + fun getVersionsForMod() { + val result = modrinth.getVersionsForMod("6kq7BzRK") + if (result is VersionResult.Error) { + result.cause?.let { + fail(result.text.key, it) + } + fail(result.text.key) + } + val versions = (result as VersionResult.Success).versions + assert(versions.isNotEmpty()) + versions.forEach { + assert(it.gameVersions.isNotEmpty()) + assertContains(it.gameVersions, "1.17.1") + assert(it.version.isNotEmpty()) + assert(it.changelog.isNotEmpty()) + assertEquals(VersionType.ALPHA, it.type) + assert(it.assets.isNotEmpty()) + it.assets.forEach { asset -> + assert(asset.filename.isNotEmpty()) + assert(asset.filename.endsWith(".jar")) + assert(asset.url.isNotEmpty()) + assert(asset.hashes.isNotEmpty()) + assertContains(asset.hashes, "sha512") + } + } + } +} \ No newline at end of file From 96114d9558614f967d26792190be11a2338feb74 Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Sat, 28 Aug 2021 13:47:43 +0200 Subject: [PATCH 02/25] Test every commit Signed-off-by: DeathsGun --- .github/workflows/{build.yml => test.yml} | 24 +++++++++++------------ build.gradle | 4 +++- 2 files changed, 15 insertions(+), 13 deletions(-) rename .github/workflows/{build.yml => test.yml} (60%) diff --git a/.github/workflows/build.yml b/.github/workflows/test.yml similarity index 60% rename from .github/workflows/build.yml rename to .github/workflows/test.yml index 18313a1..79ec88a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/test.yml @@ -1,30 +1,30 @@ -name: build -on: [ pull_request ] +name: Test commit +on: [ push ] jobs: build: - strategy: - matrix: - java: [ - 16 - ] - os: [ ubuntu-latest ] - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest steps: - name: checkout repository uses: actions/checkout@v2 - name: validate gradle wrapper uses: gradle/wrapper-validation-action@v1 - - name: setup jdk ${{ matrix.java }} + - name: setup jdk 16 uses: actions/setup-java@v1 with: - java-version: ${{ matrix.java }} + java-version: 16 - name: make gradle wrapper executable run: chmod +x ./gradlew + - name: test + run: ./gradlew test + - name: Publish Unit Test Results + uses: EnricoMi/publish-unit-test-result-action@v1 + if: always() + with: + files: build/test-results/**/*.xml - name: build run: ./gradlew build - name: capture build artifacts - if: ${{ matrix.java == '16' }} uses: actions/upload-artifact@v2 with: name: Artifacts diff --git a/build.gradle b/build.gradle index bd45a60..45d29eb 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,7 @@ dependencies { modImplementation "net.fabricmc:fabric-language-kotlin:${project.fabric_kotlin_version}" modImplementation "com.terraformersmc:modmenu:${project.modmenu_version}" - testImplementation "org.jetbrains.kotlin:kotlin-test" + testImplementation 'org.jetbrains.kotlin:kotlin-test:1.5.21' } processResources { @@ -69,10 +69,12 @@ tasks.withType(JavaCompile).configureEach { compileKotlin { kotlinOptions.jvmTarget = "16" + kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } compileTestKotlin { kotlinOptions.jvmTarget = "16" + kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } java { From b302aab5c3b8770f1cc7554a3eb00b9738a017aa Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Sat, 28 Aug 2021 18:35:37 +0200 Subject: [PATCH 03/25] Rewrote update check part Signed-off-by: DeathsGun --- .../modmanager/mixin/ModsScreenMixin.java | 8 + .../modmanager/mixin/TitleScreenMixin.java | 9 + .../xyz/deathsgun/modmanager/Initializer.kt | 26 --- .../xyz/deathsgun/modmanager/ModManager.kt | 52 +++++- .../xyz/deathsgun/modmanager/config/Config.kt | 21 ++- .../modmanager/providers/modrinth/Modrinth.kt | 9 +- .../providers/modrinth/models/SearchResult.kt | 2 +- .../deathsgun/modmanager/state/ModState.kt | 7 + .../deathsgun/modmanager/state/SavedState.kt | 7 + .../modmanager/update/UpdateManager.kt | 175 ++++++++++++++++++ src/main/resources/fabric.mod.json | 11 +- 11 files changed, 285 insertions(+), 42 deletions(-) create mode 100644 src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java create mode 100644 src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java delete mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/Initializer.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/state/ModState.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/state/SavedState.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt diff --git a/src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java b/src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java new file mode 100644 index 0000000..a2b938b --- /dev/null +++ b/src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java @@ -0,0 +1,8 @@ +package xyz.deathsgun.modmanager.mixin; + +import com.terraformersmc.modmenu.gui.ModsScreen; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(ModsScreen.class) +public class ModsScreenMixin { +} diff --git a/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java b/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java new file mode 100644 index 0000000..3f8cab4 --- /dev/null +++ b/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java @@ -0,0 +1,9 @@ +package xyz.deathsgun.modmanager.mixin; + +import net.minecraft.client.gui.screen.TitleScreen; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(TitleScreen.class) +public class TitleScreenMixin { + +} diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/Initializer.kt b/src/main/kotlin/xyz/deathsgun/modmanager/Initializer.kt deleted file mode 100644 index 6578b37..0000000 --- a/src/main/kotlin/xyz/deathsgun/modmanager/Initializer.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2021 DeathsGun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package xyz.deathsgun.modmanager - -import net.fabricmc.api.ModInitializer - -class Initializer : ModInitializer { - override fun onInitialize() { - - } - -} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt index 80f73a5..d4bfca5 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt @@ -16,11 +16,59 @@ package xyz.deathsgun.modmanager -class ModManager { +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import net.fabricmc.api.ClientModInitializer +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.client.MinecraftClient +import xyz.deathsgun.modmanager.api.provider.IModProvider +import xyz.deathsgun.modmanager.api.provider.IModUpdateProvider +import xyz.deathsgun.modmanager.config.Config +import xyz.deathsgun.modmanager.providers.modrinth.Modrinth +import xyz.deathsgun.modmanager.state.ModState +import xyz.deathsgun.modmanager.state.SavedState +import xyz.deathsgun.modmanager.update.UpdateManager + +class ModManager : ClientModInitializer { + + lateinit var config: Config + val update: UpdateManager = UpdateManager() + val provider: HashMap = HashMap() + val updateProvider: HashMap = HashMap() + private val states = ArrayList() companion object { + lateinit var modManager: ModManager + fun getVersion(): String { - return "1.0.0" + return FabricLoader.getInstance().allMods.first { it.metadata.id.equals("modmanager") }.metadata.version.friendlyString + } + + fun getMinecraftVersion(): String { + return MinecraftClient.getInstance()?.game?.version?.releaseTarget ?: "1.17.1" } } + + @OptIn(DelicateCoroutinesApi::class) + override fun onInitializeClient() { + modManager = this + config = Config("Modrinth", Config.UpdateChannel.STABLE) + val modrinth = Modrinth() + provider[modrinth.getName()] = modrinth + updateProvider[modrinth.getName()] = modrinth + GlobalScope.launch { + update.checkUpdates() + } + } + + fun setModState(fabricId: String, modId: String, state: ModState) { + this.states.removeAll { it.modId == modId || it.fabricId == fabricId } + this.states.add(SavedState(fabricId, modId, state)) + } + + fun getModState(id: String): ModState { + return this.states.find { it.modId == id || it.fabricId == id }?.state ?: ModState.DOWNLOADABLE + } + } \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt b/src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt index db2a634..43d18b9 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt @@ -19,6 +19,21 @@ package xyz.deathsgun.modmanager.config import xyz.deathsgun.modmanager.api.mod.VersionType data class Config( - var provider: String, - var updateChannel: VersionType -) + var defaultProvider: String, + var updateChannel: UpdateChannel +) { + + enum class UpdateChannel { + ALL, STABLE, UNSTABLE + } + + fun isReleaseAllowed(type: VersionType): Boolean { + if (updateChannel == UpdateChannel.ALL) { + return true + } + if (updateChannel == UpdateChannel.STABLE && type == VersionType.RELEASE) { + return true + } + return updateChannel == UpdateChannel.UNSTABLE + } +} diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt index 44c6ab5..4a7d6c3 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt @@ -19,7 +19,6 @@ package xyz.deathsgun.modmanager.providers.modrinth import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json -import net.minecraft.client.MinecraftClient import net.minecraft.text.TranslatableText import org.apache.http.client.utils.URIBuilder import org.apache.logging.log4j.LogManager @@ -58,7 +57,6 @@ class Modrinth : IModProvider, IModUpdateProvider { if (this.categories.isNotEmpty()) { return CategoriesResult.Success(this.categories) } - logger.info("Getting categories") val request = HttpRequest.newBuilder().GET().setHeader("User-Agent", "ModManager " + ModManager.getVersion()) .uri(URI.create("${baseUri}/api/v1/tag/category")).build() val response = this.http.send(request, HttpResponse.BodyHandlers.ofString()) @@ -80,7 +78,6 @@ class Modrinth : IModProvider, IModUpdateProvider { } override fun getMods(sorting: Sorting, page: Int, limit: Int): ModsResult { - logger.info("Getting a general list of mods") val builder = URIBuilder("${baseUri}/api/v1/mod") builder.addParameter("index", sorting.name.lowercase()) builder.addParameter("filters", "categories=\"fabric\"") @@ -88,7 +85,6 @@ class Modrinth : IModProvider, IModUpdateProvider { } override fun getMods(category: Category, page: Int, limit: Int): ModsResult { - logger.info("Getting category '{}' from Modrinth", category.id) val key = java.lang.String.format("%s|%d|%d", category.id, page, limit) if (this.cache.containsKey(key)) { val cachedResult = this.cache[key]!! @@ -116,7 +112,6 @@ class Modrinth : IModProvider, IModUpdateProvider { } override fun getMods(query: String, page: Int, limit: Int): ModsResult { - logger.info("Searching for '{}' in Modrinth", query) val builder = URIBuilder("${baseUri}/api/v1/mod") builder.addParameter("query", query) builder.addParameter("filters", "categories=\"fabric\" AND NOT client_side=\"unsupported\"") @@ -130,7 +125,7 @@ class Modrinth : IModProvider, IModUpdateProvider { private fun getMods(builder: URIBuilder, page: Int, limit: Int): ModsResult { builder.addParameter( "version", - String.format("versions=%s", MinecraftClient.getInstance()?.game?.version?.releaseTarget ?: "1.17.1") + String.format("versions=%s", ModManager.getMinecraftVersion()) ) builder.addParameter("offset", (page * limit).toString()) builder.addParameter("limit", limit.toString()) @@ -180,7 +175,7 @@ class Modrinth : IModProvider, IModUpdateProvider { } ModResult.Success( Mod( - id = result.id, + id = result.id.replaceFirst("local-", ""), slug = result.slug, author = mod.author, name = result.title, diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/SearchResult.kt b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/SearchResult.kt index 9f02636..e751e79 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/SearchResult.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/SearchResult.kt @@ -39,7 +39,7 @@ data class SearchResult( } mods.add( Mod( - mod.id, + mod.id.replaceFirst("local-", ""), mod.slug, mod.author, mod.title, diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/state/ModState.kt b/src/main/kotlin/xyz/deathsgun/modmanager/state/ModState.kt new file mode 100644 index 0000000..ed437c0 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/state/ModState.kt @@ -0,0 +1,7 @@ +package xyz.deathsgun.modmanager.state + +enum class ModState { + + INSTALLED, OUTDATED, DOWNLOADABLE + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/state/SavedState.kt b/src/main/kotlin/xyz/deathsgun/modmanager/state/SavedState.kt new file mode 100644 index 0000000..7e26220 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/state/SavedState.kt @@ -0,0 +1,7 @@ +package xyz.deathsgun.modmanager.state + +data class SavedState( + val fabricId: String, + val modId: String, + val state: ModState +) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt new file mode 100644 index 0000000..5743bc0 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt @@ -0,0 +1,175 @@ +package xyz.deathsgun.modmanager.update + +import com.terraformersmc.modmenu.util.mod.fabric.CustomValueUtil +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import net.fabricmc.loader.api.FabricLoader +import net.fabricmc.loader.api.SemanticVersion +import net.fabricmc.loader.api.metadata.ModMetadata +import net.fabricmc.loader.util.version.VersionDeserializer +import org.apache.logging.log4j.LogManager +import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.api.http.ModsResult +import xyz.deathsgun.modmanager.api.http.VersionResult +import xyz.deathsgun.modmanager.api.mod.Version +import xyz.deathsgun.modmanager.api.provider.IModUpdateProvider +import xyz.deathsgun.modmanager.state.ModState + + +class UpdateManager { + + private val logger = LogManager.getLogger("UpdateCheck") + private val blockedIds = arrayOf("java", "minecraft") + + suspend fun checkUpdates() = coroutineScope { + val mods = getCheckableMods() + mods.forEach { metadata -> + launch { + val configIds = getIdBy(metadata) + if (configIds == null) { + logger.info("Searching for updates for {} using fallback method", metadata.id) + checkForUpdatesManually(metadata) + return@launch + } + logger.info("Searching for updates for {} using defined mod id", metadata.id) + checkForUpdates(metadata, configIds) + } + } + } + + private fun checkForUpdatesManually(metadata: ModMetadata) { + val defaultProvider = ModManager.modManager.config.defaultProvider + val provider = ModManager.modManager.provider[defaultProvider] + if (provider == null) { + logger.warn("Default provider {} not found", defaultProvider) + return + } + val updateProvider = ModManager.modManager.updateProvider[defaultProvider] + if (updateProvider == null) { + logger.warn("Default update provider {} not found", defaultProvider) + return + } + var result = updateProvider.getVersionsForMod(metadata.id) + if (result is VersionResult.Success) { + val version = findLatestCompatible(metadata.version.friendlyString, result.versions) + if (version == null) { + logger.info("No update for {} found!", metadata.id) + ModManager.modManager.setModState(metadata.id, metadata.id, ModState.INSTALLED) + return + } + logger.info("Update for {} found [{} -> {}]", metadata.id, metadata.version.friendlyString, version.version) + ModManager.modManager.setModState(metadata.id, metadata.id, ModState.OUTDATED) + return + } + + val queryResult = provider.getMods(metadata.name, 0, 10) + if (queryResult is ModsResult.Error) { + logger.warn("Error while searching for fallback id for mod {}: ", metadata.id, queryResult.cause) + ModManager.modManager.setModState(metadata.id, metadata.id, ModState.INSTALLED) + return + } + val mod = + (queryResult as ModsResult.Success).mods.find { mod -> mod.slug == metadata.id || mod.name == metadata.name } + if (mod == null) { + logger.warn("Error while searching for fallback id for mod {}: No possible match found", metadata.id) + ModManager.modManager.setModState(metadata.id, metadata.id, ModState.INSTALLED) + return + } + result = updateProvider.getVersionsForMod(mod.id) + val versions = when (result) { + is VersionResult.Error -> { + logger.error("Error while getting versions for mod {}", metadata.id) + ModManager.modManager.setModState(metadata.id, mod.id, ModState.INSTALLED) + return + } + is VersionResult.Success -> result.versions + } + val version = findLatestCompatible(metadata.version.friendlyString, versions) + if (version == null) { + logger.info("No update for {} found!", metadata.id) + ModManager.modManager.setModState(metadata.id, mod.id, ModState.INSTALLED) + return + } + logger.info("Update for {} found [{} -> {}]", metadata.id, metadata.version.friendlyString, version.version) + ModManager.modManager.setModState(metadata.id, mod.id, ModState.OUTDATED) + } + + private fun checkForUpdates(metadata: ModMetadata, ids: Map) { + var provider: IModUpdateProvider? = null + var id: String? = null + for ((providerId, modId) in ids) { + if (!ModManager.modManager.updateProvider.containsKey(providerId)) { + logger.warn("Update provider {} for {} not found!", providerId, metadata.id) + continue + } + provider = ModManager.modManager.updateProvider[providerId]!! + id = modId + } + if (provider == null || id == null) { + logger.warn("No valid provider for {} found! Skipping", metadata.id) + ModManager.modManager.setModState(metadata.id, id ?: metadata.id, ModState.INSTALLED) + return + } + val versions = when (val result = provider.getVersionsForMod(id)) { + is VersionResult.Error -> { + logger.error("Error while getting versions for mod {}", metadata.id) + ModManager.modManager.setModState(metadata.id, id, ModState.INSTALLED) + return + } + is VersionResult.Success -> result.versions + } + val version = findLatestCompatible(metadata.version.friendlyString, versions) + if (version == null) { + logger.info("No update for {} found!", metadata.id) + ModManager.modManager.setModState(metadata.id, id, ModState.INSTALLED) + return + } + logger.info("Update for {} found [{} -> {}]", metadata.id, metadata.version.friendlyString, version.version) + ModManager.modManager.setModState(metadata.id, id, ModState.OUTDATED) + } + + private fun findLatestCompatible(installedVersion: String, versions: List): Version? { + var latest: Version? = null + var latestVersion: SemanticVersion? = null + val installVersion = + VersionDeserializer.deserializeSemantic(installedVersion.split("+")[0]) // Remove additional info from version + for (version in versions) { + if (!version.gameVersions.contains(ModManager.getMinecraftVersion()) || + ModManager.modManager.config.isReleaseAllowed(version.type) + ) { + continue + } + val ver = + VersionDeserializer.deserializeSemantic(version.version.split("+")[0]) // Remove additional info from version + if (latestVersion == null || ver > latestVersion) { + latest = version + latestVersion = ver + } + } + if (latestVersion?.compareTo(installVersion) == 0) { + return null + } + return latest + } + + private fun getIdBy(metadata: ModMetadata): Map? { + if (!metadata.containsCustomValue("modmanager")) { + return null + } + val ids = HashMap() + val map = metadata.getCustomValue("modmanager").asObject + map.forEach { + ids[it.key] = it.value.asString + } + return ids + } + + private fun getCheckableMods(): List { + return FabricLoader.getInstance().allMods.map { it.metadata }.filter { + !it.id.startsWith("fabric") && + !CustomValueUtil.getBoolean("fabric-loom:generated", it).orElse(false) && + !blockedIds.contains(it.id) + } + } + +} \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 66d7b8c..f005906 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -16,15 +16,20 @@ "license": "APL-2.0", "environment": "client", "entrypoints": { - "client": [ + "client": [ { "adapter": "kotlin", - "value": "xyz.deathsgun.modmanager.Initializer" + "value": "xyz.deathsgun.modmanager.ModManager" } ] }, + "custom": { + "modmanager": { + "Modrinth": "6kq7BzRK" + } + }, "mixins": [ - "modmanager.mixins.json" + "modmanager.mixins.json" ], "depends": { "fabricloader": ">=0.11.3", From 5ff0b801343f3eb1c83fb0b1bd8ead04e2ce27f6 Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Sat, 28 Aug 2021 19:39:36 +0200 Subject: [PATCH 04/25] Fix tests Signed-off-by: DeathsGun --- src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt index d4bfca5..dc68094 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt @@ -42,7 +42,11 @@ class ModManager : ClientModInitializer { lateinit var modManager: ModManager fun getVersion(): String { - return FabricLoader.getInstance().allMods.first { it.metadata.id.equals("modmanager") }.metadata.version.friendlyString + return try { + FabricLoader.getInstance().allMods.first { it.metadata.id.equals("modmanager") }.metadata.version.friendlyString + } catch (ignored: NoSuchElementException) { + "1.1.0+1.17.1-alpha" + } } fun getMinecraftVersion(): String { From 21c1910ab90113cd2093c549c1e997e2b0a9a9db Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Sun, 29 Aug 2021 10:06:58 +0530 Subject: [PATCH 05/25] Use proper assertion methods in ModrinthTest --- .../providers/modrinth/ModrinthTest.kt | 62 ++++++++++--------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt b/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt index 1c1301d..7839bb2 100644 --- a/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt +++ b/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt @@ -30,6 +30,8 @@ import xyz.deathsgun.modmanager.api.provider.Sorting import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertNotEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue internal class ModrinthTest { @@ -45,9 +47,9 @@ internal class ModrinthTest { fail(result.text.key) } val categories = (result as CategoriesResult.Success).categories - assert(categories.isNotEmpty()) + assertTrue(categories.isNotEmpty()) categories.forEach { - assert(it.id.isNotEmpty()) + assertTrue(it.id.isNotEmpty()) assertEquals(String.format("modmanager.category.%s", it.id), it.text.key) } } @@ -92,20 +94,20 @@ internal class ModrinthTest { } private fun checkMods(mods: List) { - assert(mods.isNotEmpty()) + assertTrue(mods.isNotEmpty()) assertEquals(mods.size, 10) mods.forEach { - assert(it.id.isNotEmpty()) - assert(it.slug.isNotEmpty()) - assert(it.name.isNotEmpty()) - assert(it.author.isNotEmpty()) - assert(it.iconUrl.isNotEmpty()) - assert(it.shortDescription.isNotEmpty()) - assert(it.categories.isNotEmpty()) + assertTrue(it.id.isNotEmpty()) + assertTrue(it.slug.isNotEmpty()) + assertTrue(it.name.isNotEmpty()) + assertTrue(it.author.isNotEmpty()) + assertTrue(it.iconUrl.isNotEmpty()) + assertTrue(it.shortDescription.isNotEmpty()) + assertTrue(it.categories.isNotEmpty()) // Only filled when getMod(id) is called - assertEquals(null, it.description, "description should be null as it's only loaded by getMod") - assertEquals(null, it.license, "description should be null as it's only loaded by getMod") + assertNull(it.description, "description should be null as it's only loaded by getMod") + assertNull(it.license, "description should be null as it's only loaded by getMod") } } @@ -126,17 +128,17 @@ internal class ModrinthTest { fail(result.text.key) } val mod = (result as ModResult.Success).mod - assert(mod.id.isNotEmpty()) - assert(mod.slug.isNotEmpty()) - assert(mod.name.isNotEmpty()) - assert(mod.author.isNotEmpty()) - assert(mod.iconUrl.isNotEmpty()) - assert(mod.shortDescription.isNotEmpty()) - assert(mod.categories.isNotEmpty()) + assertTrue(mod.id.isNotEmpty()) + assertTrue(mod.slug.isNotEmpty()) + assertTrue(mod.name.isNotEmpty()) + assertTrue(mod.author.isNotEmpty()) + assertTrue(mod.iconUrl.isNotEmpty()) + assertTrue(mod.shortDescription.isNotEmpty()) + assertTrue(mod.categories.isNotEmpty()) assertNotEquals(mod.description, null) - assert(mod.description!!.isNotEmpty()) + assertTrue(mod.description!!.isNotEmpty()) assertNotEquals(mod.license, null) - assert(mod.license!!.isNotEmpty()) + assertTrue(mod.license!!.isNotEmpty()) } @Test @@ -149,19 +151,19 @@ internal class ModrinthTest { fail(result.text.key) } val versions = (result as VersionResult.Success).versions - assert(versions.isNotEmpty()) + assertTrue(versions.isNotEmpty()) versions.forEach { - assert(it.gameVersions.isNotEmpty()) + assertTrue(it.gameVersions.isNotEmpty()) assertContains(it.gameVersions, "1.17.1") - assert(it.version.isNotEmpty()) - assert(it.changelog.isNotEmpty()) + assertTrue(it.version.isNotEmpty()) + assertTrue(it.changelog.isNotEmpty()) assertEquals(VersionType.ALPHA, it.type) - assert(it.assets.isNotEmpty()) + assertTrue(it.assets.isNotEmpty()) it.assets.forEach { asset -> - assert(asset.filename.isNotEmpty()) - assert(asset.filename.endsWith(".jar")) - assert(asset.url.isNotEmpty()) - assert(asset.hashes.isNotEmpty()) + assertTrue(asset.filename.isNotEmpty()) + assertTrue(asset.filename.endsWith(".jar")) + assertTrue(asset.url.isNotEmpty()) + assertTrue(asset.hashes.isNotEmpty()) assertContains(asset.hashes, "sha512") } } From 393cd17315ebf526479ac78d945866aab20a5f84 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Sun, 29 Aug 2021 10:25:56 +0530 Subject: [PATCH 06/25] Make iconUrl nullable --- src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt | 2 +- .../modmanager/providers/modrinth/models/ModResult.kt | 2 +- .../deathsgun/modmanager/providers/modrinth/ModrinthTest.kt | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt index 301d3a6..ac56d11 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt @@ -22,7 +22,7 @@ data class Mod( val author: String, val name: String, val shortDescription: String, - val iconUrl: String, + val iconUrl: String?, val description: String?, val license: String?, val categories: List, diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModResult.kt b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModResult.kt index ed5412a..ad1e354 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModResult.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModResult.kt @@ -28,6 +28,6 @@ data class ModResult( val title: String, val description: String, @SerialName("icon_url") - val iconUrl: String, + val iconUrl: String?, val categories: ArrayList ) diff --git a/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt b/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt index 7839bb2..d8ed30f 100644 --- a/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt +++ b/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt @@ -30,6 +30,7 @@ import xyz.deathsgun.modmanager.api.provider.Sorting import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -101,7 +102,7 @@ internal class ModrinthTest { assertTrue(it.slug.isNotEmpty()) assertTrue(it.name.isNotEmpty()) assertTrue(it.author.isNotEmpty()) - assertTrue(it.iconUrl.isNotEmpty()) + assertNotNull(it.iconUrl) assertTrue(it.shortDescription.isNotEmpty()) assertTrue(it.categories.isNotEmpty()) @@ -132,7 +133,7 @@ internal class ModrinthTest { assertTrue(mod.slug.isNotEmpty()) assertTrue(mod.name.isNotEmpty()) assertTrue(mod.author.isNotEmpty()) - assertTrue(mod.iconUrl.isNotEmpty()) + assertNotNull(mod.iconUrl) assertTrue(mod.shortDescription.isNotEmpty()) assertTrue(mod.categories.isNotEmpty()) assertNotEquals(mod.description, null) From c7a38b6cf8381db4a25764583834af65809b05a8 Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Sun, 29 Aug 2021 10:58:40 +0200 Subject: [PATCH 07/25] Added update notification - Fixed if condition in UpdateManager Signed-off-by: DeathsGun --- .../modmanager/mixin/TitleScreenMixin.java | 36 ++++++++++++++++++- .../modmanager/update/UpdateManager.kt | 6 +++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java b/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java index 3f8cab4..3e02144 100644 --- a/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java +++ b/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java @@ -1,9 +1,43 @@ package xyz.deathsgun.modmanager.mixin; +import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.screen.TitleScreen; +import net.minecraft.client.toast.SystemToast; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import xyz.deathsgun.modmanager.ModManager; +import xyz.deathsgun.modmanager.api.mod.Version; + +import java.util.Map; +import java.util.Objects; @Mixin(TitleScreen.class) -public class TitleScreenMixin { +public class TitleScreenMixin extends Screen { + + private boolean hasRun = false; + + protected TitleScreenMixin(Text title) { + super(title); + } + + @Inject(at = @At("TAIL"), method = "render") + public void onRender(MatrixStack matrices, int mouseX, int mouseY, float delta, CallbackInfo ci) { + if (hasRun) { + return; + } + hasRun = true; + Map updates = ModManager.modManager.getUpdate().getUpdates(); + if (updates.isEmpty()) { + return; + } + Objects.requireNonNull(client).getToastManager().add(new SystemToast(SystemToast.Type.TUTORIAL_HINT, + new TranslatableText("modmanager.toast.update.title"), + new TranslatableText("modmanager.toast.update.description", updates.size()))); + } } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt index 5743bc0..773399d 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt @@ -20,6 +20,7 @@ class UpdateManager { private val logger = LogManager.getLogger("UpdateCheck") private val blockedIds = arrayOf("java", "minecraft") + val updates = HashMap() suspend fun checkUpdates() = coroutineScope { val mods = getCheckableMods() @@ -59,6 +60,7 @@ class UpdateManager { } logger.info("Update for {} found [{} -> {}]", metadata.id, metadata.version.friendlyString, version.version) ModManager.modManager.setModState(metadata.id, metadata.id, ModState.OUTDATED) + this.updates[metadata.id] = version return } @@ -92,6 +94,7 @@ class UpdateManager { } logger.info("Update for {} found [{} -> {}]", metadata.id, metadata.version.friendlyString, version.version) ModManager.modManager.setModState(metadata.id, mod.id, ModState.OUTDATED) + this.updates[metadata.id] = version } private fun checkForUpdates(metadata: ModMetadata, ids: Map) { @@ -126,6 +129,7 @@ class UpdateManager { } logger.info("Update for {} found [{} -> {}]", metadata.id, metadata.version.friendlyString, version.version) ModManager.modManager.setModState(metadata.id, id, ModState.OUTDATED) + this.updates[metadata.id] = version } private fun findLatestCompatible(installedVersion: String, versions: List): Version? { @@ -135,7 +139,7 @@ class UpdateManager { VersionDeserializer.deserializeSemantic(installedVersion.split("+")[0]) // Remove additional info from version for (version in versions) { if (!version.gameVersions.contains(ModManager.getMinecraftVersion()) || - ModManager.modManager.config.isReleaseAllowed(version.type) + !ModManager.modManager.config.isReleaseAllowed(version.type) ) { continue } From 2f27ff9cf55c756f28d8ab5cd0153d5cb32c3c55 Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Sun, 29 Aug 2021 11:11:54 +0200 Subject: [PATCH 08/25] Make config loadable Signed-off-by: DeathsGun --- .../xyz/deathsgun/modmanager/ModManager.kt | 9 +++-- .../xyz/deathsgun/modmanager/config/Config.kt | 34 +++++++++++++++++++ .../modmanager/update/UpdateManager.kt | 3 +- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt index dc68094..8016d9c 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt @@ -39,8 +39,10 @@ class ModManager : ClientModInitializer { private val states = ArrayList() companion object { + @JvmStatic lateinit var modManager: ModManager + @JvmStatic fun getVersion(): String { return try { FabricLoader.getInstance().allMods.first { it.metadata.id.equals("modmanager") }.metadata.version.friendlyString @@ -49,6 +51,7 @@ class ModManager : ClientModInitializer { } } + @JvmStatic fun getMinecraftVersion(): String { return MinecraftClient.getInstance()?.game?.version?.releaseTarget ?: "1.17.1" } @@ -57,10 +60,10 @@ class ModManager : ClientModInitializer { @OptIn(DelicateCoroutinesApi::class) override fun onInitializeClient() { modManager = this - config = Config("Modrinth", Config.UpdateChannel.STABLE) + config = Config.loadConfig() val modrinth = Modrinth() - provider[modrinth.getName()] = modrinth - updateProvider[modrinth.getName()] = modrinth + provider[modrinth.getName().lowercase()] = modrinth + updateProvider[modrinth.getName().lowercase()] = modrinth GlobalScope.launch { update.checkUpdates() } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt b/src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt index 43d18b9..5fcfaf2 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt @@ -16,13 +16,47 @@ package xyz.deathsgun.modmanager.config +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import net.fabricmc.loader.api.FabricLoader import xyz.deathsgun.modmanager.api.mod.VersionType +import java.nio.charset.Charset +import java.nio.file.Files +@Serializable data class Config( var defaultProvider: String, var updateChannel: UpdateChannel ) { + companion object { + @OptIn(ExperimentalSerializationApi::class) + fun loadConfig(): Config { + return try { + val file = FabricLoader.getInstance().configDir.resolve("modmanager.json") + val data = Files.readString(file, Charset.forName("UTF-8")) + Json.decodeFromString(data) + } catch (e: Exception) { + e.printStackTrace() + saveConfig(Config("modrinth", UpdateChannel.STABLE)) + } + } + + @OptIn(ExperimentalSerializationApi::class) + private fun saveConfig(config: Config): Config { + try { + val file = FabricLoader.getInstance().configDir.resolve("modmanager.json") + val data = Json.encodeToString(config) + Files.writeString(file, data, Charset.forName("UTF-8")) + } catch (ignored: Exception) { + } + return config + } + } + enum class UpdateChannel { ALL, STABLE, UNSTABLE } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt index 773399d..2116463 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt @@ -100,7 +100,8 @@ class UpdateManager { private fun checkForUpdates(metadata: ModMetadata, ids: Map) { var provider: IModUpdateProvider? = null var id: String? = null - for ((providerId, modId) in ids) { + for ((provId, modId) in ids) { + val providerId = provId.lowercase() if (!ModManager.modManager.updateProvider.containsKey(providerId)) { logger.warn("Update provider {} for {} not found!", providerId, metadata.id) continue From 72f8c2cd7b70101eadc1f37920de42d4ee4947e3 Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Mon, 30 Aug 2021 22:41:26 +0200 Subject: [PATCH 09/25] Make mods searchable again Signed-off-by: DeathsGun --- .gitignore | 4 +- .../modmanager/mixin/ModsScreenMixin.java | 30 +++- .../xyz/deathsgun/modmanager/ModManager.kt | 8 +- .../modmanager/api/provider/IModProvider.kt | 2 +- .../modmanager/gui/ModsOverviewScreen.kt | 94 +++++++++++ .../modmanager/gui/widget/ErrorWidget.kt | 28 ++++ .../modmanager/gui/widget/ModListEntry.kt | 107 ++++++++++++ .../modmanager/gui/widget/ModListWidget.kt | 39 +++++ .../deathsgun/modmanager/icon/IconCache.kt | 157 ++++++++++++++++++ .../modmanager/providers/modrinth/Modrinth.kt | 2 +- .../modmanager/update/UpdateManager.kt | 2 +- .../assets/modmanager/lang/en_us.json | 54 +++--- .../assets/modmanager/lang/ko_kr.json | 2 +- .../assets/modmanager/lang/ru_ru.json | 2 +- .../assets/modmanager/lang/tr_tr.json | 2 +- .../assets/modmanager/lang/zh_cn.json | 54 +++--- .../providers/modrinth/ModrinthTest.kt | 10 +- 17 files changed, 524 insertions(+), 73 deletions(-) create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ErrorWidget.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListEntry.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/icon/IconCache.kt diff --git a/.gitignore b/.gitignore index 3c37caf..3dd3227 100644 --- a/.gitignore +++ b/.gitignore @@ -53,9 +53,6 @@ hs_err_pid* .AppleDouble .LSOverride -# Icon must end with two \r -Icon - # Thumbnails ._* @@ -113,6 +110,7 @@ gradle-app.setting # Common working directory run/ +logs/ # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar diff --git a/src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java b/src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java index a2b938b..98afc41 100644 --- a/src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java +++ b/src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java @@ -1,8 +1,36 @@ package xyz.deathsgun.modmanager.mixin; import com.terraformersmc.modmenu.gui.ModsScreen; +import com.terraformersmc.modmenu.gui.widget.ModMenuTexturedButtonWidget; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Identifier; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import xyz.deathsgun.modmanager.gui.ModsOverviewScreen; @Mixin(ModsScreen.class) -public class ModsScreenMixin { +public class ModsScreenMixin extends Screen { + + private static final Identifier MODMANAGER_BUTTON_LOCATION = new Identifier("modmanager", "textures/gui/install_button.png"); + @Shadow + private int paneWidth; + + protected ModsScreenMixin(Text title) { + super(title); + } + + @Inject(method = "init", at = @At("TAIL")) + public void onInit(CallbackInfo ci) { + int searchBoxWidth = this.paneWidth - 32 - 22; + this.addDrawableChild(new ModMenuTexturedButtonWidget(this.paneWidth / 2 + searchBoxWidth / 2 + 14, + 22, 20, 20, 0, 0, MODMANAGER_BUTTON_LOCATION, 32, 64, button -> { + MinecraftClient.getInstance().setScreen(new ModsOverviewScreen(this)); + }, new TranslatableText("modmanager.button.open"))); + } } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt index 8016d9c..147a39a 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt @@ -25,6 +25,7 @@ import net.minecraft.client.MinecraftClient import xyz.deathsgun.modmanager.api.provider.IModProvider import xyz.deathsgun.modmanager.api.provider.IModUpdateProvider import xyz.deathsgun.modmanager.config.Config +import xyz.deathsgun.modmanager.icon.IconCache import xyz.deathsgun.modmanager.providers.modrinth.Modrinth import xyz.deathsgun.modmanager.state.ModState import xyz.deathsgun.modmanager.state.SavedState @@ -32,11 +33,12 @@ import xyz.deathsgun.modmanager.update.UpdateManager class ModManager : ClientModInitializer { + private val states = ArrayList() lateinit var config: Config val update: UpdateManager = UpdateManager() + val icons: IconCache = IconCache() val provider: HashMap = HashMap() val updateProvider: HashMap = HashMap() - private val states = ArrayList() companion object { @JvmStatic @@ -78,4 +80,8 @@ class ModManager : ClientModInitializer { return this.states.find { it.modId == id || it.fabricId == id }?.state ?: ModState.DOWNLOADABLE } + fun getSelectedProvider(): IModProvider? { + return this.provider[config.defaultProvider] + } + } \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt index 8e0526c..d12e87c 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt @@ -68,7 +68,7 @@ interface IModProvider { * @param limit the amount of mods to return * @return a list of mods matching the search term */ - fun getMods(query: String, page: Int, limit: Int): ModsResult + fun search(query: String, page: Int, limit: Int): ModsResult /** * Returns a more detailed representation of the mod diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt new file mode 100644 index 0000000..3bc8b62 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.gui + +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.widget.TextFieldWidget +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.text.TranslatableText +import org.lwjgl.glfw.GLFW +import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.api.http.ModsResult +import xyz.deathsgun.modmanager.gui.widget.ErrorWidget +import xyz.deathsgun.modmanager.gui.widget.ModListWidget + +class ModsOverviewScreen(private val previousScreen: Screen) : Screen(TranslatableText("modmanager.title.overview")) { + + private var query: String = "" + private lateinit var errorWidget: ErrorWidget + private lateinit var searchField: TextFieldWidget + private lateinit var modList: ModListWidget + + override fun init() { + client!!.keyboard.setRepeatEvents(true) + searchField = this.addSelectableChild( + TextFieldWidget( + textRenderer, + 10, + 10, + 100, + 20, + TranslatableText("modmanager.search") + ) + ) + searchField.setChangedListener { this.query = it } + errorWidget = ErrorWidget(client!!, 10, 35, width - 130, height - 150) + modList = addSelectableChild(ModListWidget(client!!, width - 130, height, 35, height - 10, 36)) + modList.setLeftPos(10) + //TODO: Sorting selector + //TODO: Paging + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + if (this.searchField.isFocused && keyCode == GLFW.GLFW_KEY_ENTER) { + val provider = ModManager.modManager.getSelectedProvider() ?: return true + return when (val result = provider.search(this.query, 0, 20)) { + is ModsResult.Error -> { + this.errorWidget.error = result.text + true + } + is ModsResult.Success -> { + this.errorWidget.error = null + this.modList.setMods(result.mods) + true + } + } + } + return super.keyPressed(keyCode, scanCode, modifiers) + } + + override fun tick() { + super.tick() + this.searchField.tick() + } + + override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { + this.renderBackground(matrices) + this.errorWidget.render(matrices, mouseX, mouseY, delta) + if (this.errorWidget.error == null) { + this.modList.render(matrices, mouseX, mouseY, delta) + } + this.searchField.render(matrices, mouseX, mouseY, delta) + super.render(matrices, mouseX, mouseY, delta) + } + + override fun onClose() { + super.onClose() + ModManager.modManager.icons.destroyAll() + client!!.setScreen(previousScreen) + } +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ErrorWidget.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ErrorWidget.kt new file mode 100644 index 0000000..0424c70 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ErrorWidget.kt @@ -0,0 +1,28 @@ +package xyz.deathsgun.modmanager.gui.widget + +import com.terraformersmc.modmenu.util.DrawingUtil +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gui.DrawableHelper +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.text.TranslatableText + +class ErrorWidget( + private val client: MinecraftClient, + private val x: Int, + private val y: Int, + private val width: Int, + private val height: Int +) : + DrawableHelper() { + + var error: TranslatableText? = null + + fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { + if (error == null) { + return + } + val lines: Int = height / client.textRenderer.fontHeight + DrawingUtil.drawWrappedString(matrices, error!!.asString(), x, y, width, lines, 0xFFFFFF) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListEntry.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListEntry.kt new file mode 100644 index 0000000..2d73349 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListEntry.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.gui.widget + +import com.mojang.blaze3d.systems.RenderSystem +import com.terraformersmc.modmenu.util.DrawingUtil +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gui.DrawableHelper +import net.minecraft.client.gui.widget.AlwaysSelectedEntryListWidget +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.text.* +import net.minecraft.util.Language +import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.api.mod.Mod +import xyz.deathsgun.modmanager.state.ModState + + +class ModListEntry(private val client: MinecraftClient, val mod: Mod) : + AlwaysSelectedEntryListWidget.Entry() { + + private val state = ModManager.modManager.getModState(mod.id) + + override fun render( + matrices: MatrixStack?, index: Int, y: Int, x: Int, entryWidth: Int, entryHeight: Int, + mouseX: Int, mouseY: Int, hovered: Boolean, tickDelta: Float + ) { + val iconSize = 32 + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f) + ModManager.modManager.icons.bindIconTexture(mod) + RenderSystem.enableBlend() + DrawableHelper.drawTexture(matrices, x, y, 0.0f, 0.0f, iconSize, iconSize, iconSize, iconSize) + RenderSystem.disableBlend() + val name: Text = LiteralText(mod.name) + var trimmedName: StringVisitable = name + var maxNameWidth = entryWidth - iconSize - 3 + val font = this.client.textRenderer + var primaryColor = 0xFFFFFF + var secondaryColor = 0xFFFFFF + var badgeText: OrderedText? = null + if (state == ModState.INSTALLED) { + primaryColor = 0xff0e2a55.toInt() + secondaryColor = 0xff2b4b7c.toInt() + badgeText = TranslatableText("modmanager.badge.installed").asOrderedText() + maxNameWidth -= font.getWidth(badgeText) + 6 + } else if (state == ModState.OUTDATED) { + primaryColor = 0xff530C17.toInt() + secondaryColor = 0xff841426.toInt() + badgeText = TranslatableText("modmanager.badge.outdated").asOrderedText() + maxNameWidth -= font.getWidth(badgeText) + 6 + } + + val textWidth = font.getWidth(name) + if (textWidth > maxNameWidth) { + val ellipsis = StringVisitable.plain("...") + trimmedName = + StringVisitable.concat(font.trimToWidth(name, maxNameWidth - font.getWidth(ellipsis)), ellipsis) + } + font.draw( + matrices, + Language.getInstance().reorder(trimmedName), + (x + iconSize + 3).toFloat(), + (y + 1).toFloat(), + 0xFFFFFF + ) + if (badgeText != null) { + DrawingUtil.drawBadge( + matrices, + x + iconSize + 3 + textWidth + 3, + y + 1, + font.getWidth(badgeText) + 6, + badgeText, + secondaryColor, + primaryColor, + 0xFFFFFF + ) + } + + DrawingUtil.drawWrappedString( + matrices, + mod.shortDescription, + (x + iconSize + 3 + 4), + (y + client.textRenderer.fontHeight + 4), + entryWidth - iconSize - 7, + 2, + 0x808080 + ) + } + + override fun getNarration(): Text { + return LiteralText(mod.name) + } + +} diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt new file mode 100644 index 0000000..d16acaf --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.gui.widget + +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gui.widget.AlwaysSelectedEntryListWidget +import xyz.deathsgun.modmanager.api.mod.Mod + +class ModListWidget(client: MinecraftClient, width: Int, height: Int, top: Int, bottom: Int, itemHeight: Int) : + AlwaysSelectedEntryListWidget( + client, + width, + height, + top, + bottom, + itemHeight + ) { //TODO: Mojang's implementation is still broken replace it with custom one + + fun setMods(mods: List) { + mods.forEach { + this.addEntry(ModListEntry(client, it)) + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/icon/IconCache.kt b/src/main/kotlin/xyz/deathsgun/modmanager/icon/IconCache.kt new file mode 100644 index 0000000..76a7ae2 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/icon/IconCache.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.icon + +import com.mojang.blaze3d.systems.RenderSystem +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.client.MinecraftClient +import net.minecraft.client.texture.NativeImage +import net.minecraft.client.texture.NativeImageBackedTexture +import net.minecraft.util.Identifier +import org.apache.logging.log4j.LogManager +import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.api.mod.Mod +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.nio.file.Files +import java.nio.file.Path +import java.util.stream.Collectors +import kotlin.io.path.name + +class IconCache { + + private val logger = LogManager.getLogger("IconCache") + private val unknownIcon = Identifier("textures/misc/unknown_pack.png") + private val loadingIcon = Identifier("modmanager", "textures/gui/loading.png") + private val iconsDir = FabricLoader.getInstance().gameDir.resolve(".icons") + private val state: ArrayList + private val icons = HashMap() + private val downloading = ArrayList() + private val http = HttpClient.newHttpClient() + + init { + state = loadCache() + } + + @OptIn(DelicateCoroutinesApi::class) + fun downloadIcon(mod: Mod) { + if (mod.iconUrl == null) { + state.add(mod.id) + icons[mod.id] = unknownIcon + return + } + if (downloading.contains(mod.id)) { + return + } + downloading.add(mod.id) + GlobalScope.launch { + download(mod) + } + } + + fun bindIconTexture(mod: Mod) { + if (!hasIcon(mod)) { + RenderSystem.setShaderTexture(0, loadingIcon) + downloadIcon(mod) + return + } + val icon = getIcon(mod) + if (icon == null) { + RenderSystem.setShaderTexture(0, loadingIcon) + downloadIcon(mod) + return + } + RenderSystem.setShaderTexture(0, icon) + } + + fun destroyAll() { + icons.values.forEach { + MinecraftClient.getInstance().textureManager.destroyTexture(it) + } + } + + private fun hasIcon(mod: Mod): Boolean { + return state.contains(mod.id) + } + + private fun getIcon(mod: Mod): Identifier? { + if (!hasIcon(mod)) { + return unknownIcon + } + if (icons.containsKey(mod.id)) { + return icons[mod.id]!! + } + val iconFile = iconsDir.resolve(mod.id) + if (Files.exists(iconFile)) { + return readIcon(mod.id, iconFile) + } + return null + } + + private fun download(mod: Mod) { + if (mod.iconUrl == null) { + return + } + try { + val request = HttpRequest.newBuilder(URI.create(mod.iconUrl)).GET() + .setHeader("User-Agent", "ModMenu " + ModManager.getVersion()).build() + val response = http.send(request, HttpResponse.BodyHandlers.ofByteArray()) + if (response.statusCode() != 200) { + logger.error( + "Failed to download icon for {} ! received status code: {}", mod.slug, + response.statusCode() + ) + state.add(mod.id) + icons[mod.id] = unknownIcon + downloading.removeIf { it == mod.id } + return + } + val iconFile = iconsDir.resolve(mod.id) + Files.write(iconFile, response.body()) + readIcon(mod.id, iconFile) + state.add(mod.id) + } catch (e: Exception) { + logger.error(e) + state.add(mod.id) + icons[mod.id] = unknownIcon + } + downloading.removeIf { it == mod.id } + } + + private fun readIcon(modId: String, file: Path): Identifier { + val image = NativeImage.read(Files.newInputStream(file)) + val identifier = Identifier("modmanager", "icons/${modId.lowercase()}") + MinecraftClient.getInstance().textureManager.registerTexture(identifier, NativeImageBackedTexture(image)) + icons[modId] = identifier + return identifier + } + + private fun loadCache(): ArrayList { + return try { + ArrayList(Files.list(iconsDir).map { it.name }.collect(Collectors.toList())) + } catch (e: Exception) { + Files.createDirectories(iconsDir) + ArrayList() + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt index 4a7d6c3..c60e5f0 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt @@ -111,7 +111,7 @@ class Modrinth : IModProvider, IModUpdateProvider { } } - override fun getMods(query: String, page: Int, limit: Int): ModsResult { + override fun search(query: String, page: Int, limit: Int): ModsResult { val builder = URIBuilder("${baseUri}/api/v1/mod") builder.addParameter("query", query) builder.addParameter("filters", "categories=\"fabric\" AND NOT client_side=\"unsupported\"") diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt index 2116463..227cb9c 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt @@ -64,7 +64,7 @@ class UpdateManager { return } - val queryResult = provider.getMods(metadata.name, 0, 10) + val queryResult = provider.search(metadata.name, 0, 10) if (queryResult is ModsResult.Error) { logger.warn("Error while searching for fallback id for mod {}: ", metadata.id, queryResult.cause) ModManager.modManager.setModState(metadata.id, metadata.id, ModState.INSTALLED) diff --git a/src/main/resources/assets/modmanager/lang/en_us.json b/src/main/resources/assets/modmanager/lang/en_us.json index e7e851c..2fb744f 100644 --- a/src/main/resources/assets/modmanager/lang/en_us.json +++ b/src/main/resources/assets/modmanager/lang/en_us.json @@ -3,31 +3,31 @@ "modmanager.badge.outdated": "Outdated", "modmanager.categories": "Categories", "modmanager.categories.updatable": "Updatable Mods", - "modmanager.category.technology": "Technology", - "modmanager.category.adventure": "Adventure", - "modmanager.category.magic": "Magic", - "modmanager.category.utility": "Utility", - "modmanager.category.decoration": "Decoration", - "modmanager.category.library": "Library", - "modmanager.category.cursed": "Cursed", - "modmanager.category.worldgen": "World Gen", - "modmanager.category.storage": "Storage", - "modmanager.category.food": "Food", - "modmanager.category.equipment": "Equipment", - "modmanager.category.misc": "Miscellaneous", - "modmanager.details.author": "By %s", - "modmanager.message.goBackAndTryAgain": "Go Back & Try Again", - "modmanager.message.install": "Install", - "modmanager.message.installing": "Installing...", - "modmanager.message.update": "Update", - "modmanager.message.updating": "Updating...", - "modmanager.message.remove": "Remove", - "modmanager.message.removing": "Removing...", - "modmanager.message.search": "Search...", - "modmanager.message.showError": "Show Error", - "modmanager.page.next": "Next Page", - "modmanager.page.previous": "Previous Page", - "modmanager.title": "Browse Mods", - "modmanager.toast.update.title": "Updates Available!", - "modmanager.toast.update.description": "%d mods can be updated" + "modmanager.category.technology": "Technology", + "modmanager.category.adventure": "Adventure", + "modmanager.category.magic": "Magic", + "modmanager.category.utility": "Utility", + "modmanager.category.decoration": "Decoration", + "modmanager.category.library": "Library", + "modmanager.category.cursed": "Cursed", + "modmanager.category.worldgen": "World Gen", + "modmanager.category.storage": "Storage", + "modmanager.category.food": "Food", + "modmanager.category.equipment": "Equipment", + "modmanager.category.misc": "Miscellaneous", + "modmanager.details.author": "By %s", + "modmanager.message.goBackAndTryAgain": "Go Back & Try Again", + "modmanager.message.install": "Install", + "modmanager.message.installing": "Installing...", + "modmanager.message.update": "Update", + "modmanager.message.updating": "Updating...", + "modmanager.message.remove": "Remove", + "modmanager.message.removing": "Removing...", + "modmanager.search": "Search...", + "modmanager.message.showError": "Show Error", + "modmanager.page.next": "Next Page", + "modmanager.page.previous": "Previous Page", + "modmanager.title": "Browse Mods", + "modmanager.toast.update.title": "Updates Available!", + "modmanager.toast.update.description": "%d mods can be updated" } \ No newline at end of file diff --git a/src/main/resources/assets/modmanager/lang/ko_kr.json b/src/main/resources/assets/modmanager/lang/ko_kr.json index cc2b749..4b1037a 100644 --- a/src/main/resources/assets/modmanager/lang/ko_kr.json +++ b/src/main/resources/assets/modmanager/lang/ko_kr.json @@ -23,7 +23,7 @@ "modmanager.message.updating": "업데이트중...", "modmanager.message.remove": "삭제", "modmanager.message.removing": "삭제중...", - "modmanager.message.search": "검색...", + "modmanager.search": "검색...", "modmanager.message.showError": "에러 보이기", "modmanager.page.next": "다음", "modmanager.page.previous": "이전", diff --git a/src/main/resources/assets/modmanager/lang/ru_ru.json b/src/main/resources/assets/modmanager/lang/ru_ru.json index b13dd87..0b33c6e 100644 --- a/src/main/resources/assets/modmanager/lang/ru_ru.json +++ b/src/main/resources/assets/modmanager/lang/ru_ru.json @@ -23,7 +23,7 @@ "modmanager.message.updating": "Обновление...", "modmanager.message.remove": "Удалить", "modmanager.message.removing": "Удаление...", - "modmanager.message.search": "Поиск...", + "modmanager.search": "Поиск...", "modmanager.message.showError": "Просмотр ошибки", "modmanager.page.next": "Следующая страница", "modmanager.page.previous": "Предыдущая страница", diff --git a/src/main/resources/assets/modmanager/lang/tr_tr.json b/src/main/resources/assets/modmanager/lang/tr_tr.json index 16957c8..7776bce 100644 --- a/src/main/resources/assets/modmanager/lang/tr_tr.json +++ b/src/main/resources/assets/modmanager/lang/tr_tr.json @@ -23,7 +23,7 @@ "modmanager.message.updating": "Güncelleniyor...", "modmanager.message.remove": "Kaldır", "modmanager.message.removing": "Kaldırılıyor...", - "modmanager.message.search": "Mod Ara...", + "modmanager.search": "Mod Ara...", "modmanager.message.showError": "Hatayı Göster", "modmanager.page.next": "Sıradaki Sayfa", "modmanager.page.previous": "Önceki Sayfa", diff --git a/src/main/resources/assets/modmanager/lang/zh_cn.json b/src/main/resources/assets/modmanager/lang/zh_cn.json index 72b726e..072f031 100644 --- a/src/main/resources/assets/modmanager/lang/zh_cn.json +++ b/src/main/resources/assets/modmanager/lang/zh_cn.json @@ -3,31 +3,31 @@ "modmanager.badge.outdated": "可更新", "modmanager.categories": "分类", "modmanager.categories.updatable": "可更新", - "modmanager.category.technology": "科技", - "modmanager.category.adventure": "冒险", - "modmanager.category.magic": "魔法", - "modmanager.category.utility": "辅助", - "modmanager.category.decoration": "装饰", - "modmanager.category.library": "前置", - "modmanager.category.cursed": "魔改", - "modmanager.category.worldgen": "世界生成", - "modmanager.category.storage": "储存", - "modmanager.category.food": "食物", - "modmanager.category.equipment": "装备", - "modmanager.category.misc": "杂项", - "modmanager.details.author": "作者:%s", - "modmanager.message.goBackAndTryAgain": "返回并重试", - "modmanager.message.install": "安装", - "modmanager.message.installing": "安装中...", - "modmanager.message.update": "更新", - "modmanager.message.updating": "更新中...", - "modmanager.message.remove": "卸载", - "modmanager.message.removing": "卸载中...", - "modmanager.message.search": "搜索...", - "modmanager.message.showError": "查看错误信息", - "modmanager.page.next": "下一页", - "modmanager.page.previous": "上一页", - "modmanager.title": "浏览模组", - "modmanager.toast.update.title": "模组可更新!", - "modmanager.toast.update.description": "%d个模组有更新可用" + "modmanager.category.technology": "科技", + "modmanager.category.adventure": "冒险", + "modmanager.category.magic": "魔法", + "modmanager.category.utility": "辅助", + "modmanager.category.decoration": "装饰", + "modmanager.category.library": "前置", + "modmanager.category.cursed": "魔改", + "modmanager.category.worldgen": "世界生成", + "modmanager.category.storage": "储存", + "modmanager.category.food": "食物", + "modmanager.category.equipment": "装备", + "modmanager.category.misc": "杂项", + "modmanager.details.author": "作者:%s", + "modmanager.message.goBackAndTryAgain": "返回并重试", + "modmanager.message.install": "安装", + "modmanager.message.installing": "安装中...", + "modmanager.message.update": "更新", + "modmanager.message.updating": "更新中...", + "modmanager.message.remove": "卸载", + "modmanager.message.removing": "卸载中...", + "modmanager.search": "搜索...", + "modmanager.message.showError": "查看错误信息", + "modmanager.page.next": "下一页", + "modmanager.page.previous": "上一页", + "modmanager.title": "浏览模组", + "modmanager.toast.update.title": "模组可更新!", + "modmanager.toast.update.description": "%d个模组有更新可用" } \ No newline at end of file diff --git a/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt b/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt index d8ed30f..373c0f6 100644 --- a/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt +++ b/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt @@ -18,7 +18,6 @@ package xyz.deathsgun.modmanager.providers.modrinth import net.minecraft.text.TranslatableText import org.junit.jupiter.api.Test -import org.junit.jupiter.api.fail import xyz.deathsgun.modmanager.api.http.CategoriesResult import xyz.deathsgun.modmanager.api.http.ModResult import xyz.deathsgun.modmanager.api.http.ModsResult @@ -27,12 +26,7 @@ import xyz.deathsgun.modmanager.api.mod.Category import xyz.deathsgun.modmanager.api.mod.Mod import xyz.deathsgun.modmanager.api.mod.VersionType import xyz.deathsgun.modmanager.api.provider.Sorting -import kotlin.test.assertContains -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue +import kotlin.test.* internal class ModrinthTest { @@ -83,7 +77,7 @@ internal class ModrinthTest { @Test fun getModsByQuery() { - val result = modrinth.getMods("Mod", 0, 10) + val result = modrinth.search("Mod", 0, 10) if (result is ModsResult.Error) { result.cause?.let { fail(result.text.key, it) From 3175816f19ca63e23bdf4072924b15038d0e876a Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Tue, 31 Aug 2021 19:49:54 +0200 Subject: [PATCH 10/25] Using own List implementation Signed-off-by: DeathsGun --- .../modmanager/api/gui/list/IListScreen.kt | 13 ++ .../modmanager/api/gui/list/ListWidget.kt | 147 ++++++++++++++++++ .../modmanager/gui/ModsOverviewScreen.kt | 19 ++- .../modmanager/gui/widget/ModListEntry.kt | 6 +- .../modmanager/gui/widget/ModListWidget.kt | 27 ++-- 5 files changed, 196 insertions(+), 16 deletions(-) create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/IListScreen.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/ListWidget.kt diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/IListScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/IListScreen.kt new file mode 100644 index 0000000..dbd1754 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/IListScreen.kt @@ -0,0 +1,13 @@ +package xyz.deathsgun.modmanager.api.gui.list + +import net.minecraft.client.gui.Element + +interface IListScreen { + + fun getFocused(): Element? + + fun updateSelectedEntry(widget: Any, entry: E?) + + fun getEntry(widget: Any): E? + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/ListWidget.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/ListWidget.kt new file mode 100644 index 0000000..a2a3468 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/ListWidget.kt @@ -0,0 +1,147 @@ +package xyz.deathsgun.modmanager.api.gui.list + +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder +import net.minecraft.client.gui.widget.AlwaysSelectedEntryListWidget +import net.minecraft.client.render.GameRenderer +import net.minecraft.client.render.Tessellator +import net.minecraft.client.render.VertexFormat +import net.minecraft.client.render.VertexFormats +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.util.math.MathHelper +import java.util.* +import kotlin.math.max + +/** + * Using ModMenu's implementation because the implementation from + * Mojang is broken. [com.terraformersmc.modmenu.gui.ModsScreen]. + * All credits for this code go to the Terraformer's + */ +abstract class ListWidget>( + client: MinecraftClient, + width: Int, + height: Int, + top: Int, + bottom: Int, + itemHeight: Int, + protected val parent: IListScreen +) : AlwaysSelectedEntryListWidget(client, width, height, top, bottom, itemHeight) { + + protected var selectedId: String? = null + private var scrolling = false + + abstract fun init() + + override fun isFocused(): Boolean { + return parent.getFocused() == this + } + + override fun setSelected(entry: E?) { + super.setSelected(entry) + selectedId = entry?.id + parent.updateSelectedEntry(this, selectedOrNull) + } + + override fun isSelectedEntry(index: Int): Boolean { + return Objects.equals(getEntry(index).id, selectedId) + } + + override fun addEntry(entry: E): Int { + val i = super.addEntry(entry) + if (entry.id == selectedId) { + setSelected(entry) + } + return i + } + + override fun renderList(matrices: MatrixStack?, x: Int, y: Int, mouseX: Int, mouseY: Int, delta: Float) { + val itemCount = this.entryCount + val tessellator = Tessellator.getInstance() + val buffer = tessellator.buffer + + for (index in 0 until itemCount) { + val entryTop = getRowTop(index) + 2 + val entryBottom = getRowTop(index) + itemHeight + if (entryBottom >= top && entryTop <= bottom) { + val entryHeight = itemHeight - 4 + val entry: Entry = getEntry(index) + val rowWidth = this.rowWidth + var entryLeft: Int + if (isSelectedEntry(index)) { + entryLeft = rowLeft - 2 + val selectionRight = x + rowWidth + 2 + RenderSystem.disableTexture() + val color = if (this.isFocused) 1.0f else 0.5f + RenderSystem.setShader { GameRenderer.getPositionShader() } + RenderSystem.setShaderColor(color, color, color, 1.0f) + val matrix = matrices!!.peek().model + buffer.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION) + buffer.vertex(matrix, entryLeft.toFloat(), (entryTop + entryHeight + 2).toFloat(), 0.0f).next() + buffer.vertex(matrix, selectionRight.toFloat(), (entryTop + entryHeight + 2).toFloat(), 0.0f).next() + buffer.vertex(matrix, selectionRight.toFloat(), (entryTop - 2).toFloat(), 0.0f).next() + buffer.vertex(matrix, entryLeft.toFloat(), (entryTop - 2).toFloat(), 0.0f).next() + tessellator.draw() + RenderSystem.setShader { GameRenderer.getPositionShader() } + RenderSystem.setShaderColor(0.0f, 0.0f, 0.0f, 1.0f) + buffer.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION) + buffer.vertex(matrix, (entryLeft + 1).toFloat(), (entryTop + entryHeight + 1).toFloat(), 0.0f) + .next() + buffer.vertex(matrix, (selectionRight - 1).toFloat(), (entryTop + entryHeight + 1).toFloat(), 0.0f) + .next() + buffer.vertex(matrix, (selectionRight - 1).toFloat(), (entryTop - 1).toFloat(), 0.0f).next() + buffer.vertex(matrix, (entryLeft + 1).toFloat(), (entryTop - 1).toFloat(), 0.0f).next() + tessellator.draw() + RenderSystem.enableTexture() + } + entryLeft = this.rowLeft + entry.render( + matrices, index, entryTop, entryLeft, rowWidth, entryHeight, mouseX, mouseY, this.isMouseOver( + mouseX.toDouble(), mouseY.toDouble() + ) && Objects.equals(this.getEntryAtPos(mouseX, mouseY), entry), delta + ) + } + } + } + + override fun getRowWidth(): Int { + return width - if (max(0, this.maxPosition - (bottom - top - 4)) > 0) 18 else 12 + } + + override fun getRowLeft(): Int { + return left + 6 + } + + override fun getScrollbarPositionX(): Int { + return left + width - 6 + } + + override fun getMaxPosition(): Int { + return super.getMaxPosition() + 4 + } + + override fun appendNarrations(builder: NarrationMessageBuilder?) { + super.appendNarrations(builder) + } + + fun isSelectedEntry(entry: Entry): Boolean { + return selectedId == entry.id + } + + fun getEntryAtPos(x: Int, y: Int): Entry? { + val int5 = MathHelper.floor(y - top.toDouble()) - headerHeight + scrollAmount.toInt() - 4 + val index = int5 / itemHeight + return if (x < this.scrollbarPositionX.toDouble() && x >= rowLeft.toDouble() && x <= (rowLeft + rowWidth).toDouble() && index >= 0 + && int5 >= 0 && index < this.entryCount + ) children()[index] else null + } + + abstract class Entry>(open val list: ListWidget, val id: String) : + AlwaysSelectedEntryListWidget.Entry() { + @Suppress("UNCHECKED_CAST") + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + list.setSelected(this as E) + return true + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt index 3bc8b62..f94195c 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt @@ -16,17 +16,20 @@ package xyz.deathsgun.modmanager.gui +import net.minecraft.client.gui.Element import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.widget.TextFieldWidget import net.minecraft.client.util.math.MatrixStack import net.minecraft.text.TranslatableText import org.lwjgl.glfw.GLFW import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.api.gui.list.IListScreen import xyz.deathsgun.modmanager.api.http.ModsResult import xyz.deathsgun.modmanager.gui.widget.ErrorWidget import xyz.deathsgun.modmanager.gui.widget.ModListWidget -class ModsOverviewScreen(private val previousScreen: Screen) : Screen(TranslatableText("modmanager.title.overview")) { +class ModsOverviewScreen(private val previousScreen: Screen) : Screen(TranslatableText("modmanager.title.overview")), + IListScreen { private var query: String = "" private lateinit var errorWidget: ErrorWidget @@ -47,7 +50,7 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab ) searchField.setChangedListener { this.query = it } errorWidget = ErrorWidget(client!!, 10, 35, width - 130, height - 150) - modList = addSelectableChild(ModListWidget(client!!, width - 130, height, 35, height - 10, 36)) + modList = addSelectableChild(ModListWidget(client!!, width - 130, height, 35, height - 10, 36, this)) modList.setLeftPos(10) //TODO: Sorting selector //TODO: Paging @@ -71,6 +74,18 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab return super.keyPressed(keyCode, scanCode, modifiers) } + override fun getFocused(): Element? { + return super.getFocused() + } + + override fun updateSelectedEntry(widget: Any, entry: E?) { + + } + + override fun getEntry(widget: Any): E? { + return null + } + override fun tick() { super.tick() this.searchField.tick() diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListEntry.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListEntry.kt index 2d73349..d33250f 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListEntry.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListEntry.kt @@ -20,17 +20,17 @@ import com.mojang.blaze3d.systems.RenderSystem import com.terraformersmc.modmenu.util.DrawingUtil import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.DrawableHelper -import net.minecraft.client.gui.widget.AlwaysSelectedEntryListWidget import net.minecraft.client.util.math.MatrixStack import net.minecraft.text.* import net.minecraft.util.Language import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.api.gui.list.ListWidget import xyz.deathsgun.modmanager.api.mod.Mod import xyz.deathsgun.modmanager.state.ModState -class ModListEntry(private val client: MinecraftClient, val mod: Mod) : - AlwaysSelectedEntryListWidget.Entry() { +class ModListEntry(private val client: MinecraftClient, override val list: ModListWidget, val mod: Mod) : + ListWidget.Entry(list, mod.id) { private val state = ModManager.modManager.getModState(mod.id) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt index d16acaf..4877d59 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt @@ -17,22 +17,27 @@ package xyz.deathsgun.modmanager.gui.widget import net.minecraft.client.MinecraftClient -import net.minecraft.client.gui.widget.AlwaysSelectedEntryListWidget +import xyz.deathsgun.modmanager.api.gui.list.IListScreen +import xyz.deathsgun.modmanager.api.gui.list.ListWidget import xyz.deathsgun.modmanager.api.mod.Mod -class ModListWidget(client: MinecraftClient, width: Int, height: Int, top: Int, bottom: Int, itemHeight: Int) : - AlwaysSelectedEntryListWidget( - client, - width, - height, - top, - bottom, - itemHeight - ) { //TODO: Mojang's implementation is still broken replace it with custom one +class ModListWidget( + client: MinecraftClient, + width: Int, + height: Int, + top: Int, + bottom: Int, + itemHeight: Int, + parent: IListScreen +) : ListWidget(client, width, height, top, bottom, itemHeight, parent) { + + override fun init() { + + } fun setMods(mods: List) { mods.forEach { - this.addEntry(ModListEntry(client, it)) + this.addEntry(ModListEntry(client, this, it)) } } From 1f45f133cefb87e8e7cbf2847b62d11f861d94aa Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Wed, 1 Sep 2021 12:07:09 +0200 Subject: [PATCH 11/25] Rewrote IconCache which now fixes issues with mixed up icons Signed-off-by: DeathsGun --- .../modmanager/gui/ModsOverviewScreen.kt | 32 ++++- .../modmanager/gui/widget/ModListEntry.kt | 2 +- .../deathsgun/modmanager/icon/IconCache.kt | 129 ++++++------------ .../modmanager/providers/modrinth/Modrinth.kt | 8 +- 4 files changed, 78 insertions(+), 93 deletions(-) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt index f94195c..f027cf2 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt @@ -16,6 +16,9 @@ package xyz.deathsgun.modmanager.gui +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import net.minecraft.client.gui.Element import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.widget.TextFieldWidget @@ -25,17 +28,21 @@ import org.lwjgl.glfw.GLFW import xyz.deathsgun.modmanager.ModManager import xyz.deathsgun.modmanager.api.gui.list.IListScreen import xyz.deathsgun.modmanager.api.http.ModsResult +import xyz.deathsgun.modmanager.api.provider.Sorting import xyz.deathsgun.modmanager.gui.widget.ErrorWidget +import xyz.deathsgun.modmanager.gui.widget.ModListEntry import xyz.deathsgun.modmanager.gui.widget.ModListWidget class ModsOverviewScreen(private val previousScreen: Screen) : Screen(TranslatableText("modmanager.title.overview")), IListScreen { private var query: String = "" + private var selectedMod: ModListEntry? = null private lateinit var errorWidget: ErrorWidget private lateinit var searchField: TextFieldWidget private lateinit var modList: ModListWidget + @OptIn(DelicateCoroutinesApi::class) override fun init() { client!!.keyboard.setRepeatEvents(true) searchField = this.addSelectableChild( @@ -50,8 +57,20 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab ) searchField.setChangedListener { this.query = it } errorWidget = ErrorWidget(client!!, 10, 35, width - 130, height - 150) - modList = addSelectableChild(ModListWidget(client!!, width - 130, height, 35, height - 10, 36, this)) + modList = addSelectableChild(ModListWidget(client!!, width - 130, height, 35, height - 50, 36, this)) modList.setLeftPos(10) + GlobalScope.launch { + val provider = ModManager.modManager.getSelectedProvider() ?: return@launch + when (val result = provider.getMods(Sorting.RELEVANCE, 0, 20)) { + is ModsResult.Error -> { + errorWidget.error = result.text + } + is ModsResult.Success -> { + errorWidget.error = null + modList.setMods(result.mods) + } + } + } //TODO: Sorting selector //TODO: Paging } @@ -79,7 +98,16 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab } override fun updateSelectedEntry(widget: Any, entry: E?) { - + if (widget is ModListWidget) { + if (entry == null) { + return + } + if (selectedMod == entry) { + return // TODO: Open detail view + } + selectedMod = entry as ModListEntry + return + } } override fun getEntry(widget: Any): E? { diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListEntry.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListEntry.kt index d33250f..a2bd334 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListEntry.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListEntry.kt @@ -40,7 +40,7 @@ class ModListEntry(private val client: MinecraftClient, override val list: ModLi ) { val iconSize = 32 RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f) - ModManager.modManager.icons.bindIconTexture(mod) + ModManager.modManager.icons.bindIcon(mod) RenderSystem.enableBlend() DrawableHelper.drawTexture(matrices, x, y, 0.0f, 0.0f, iconSize, iconSize, iconSize, iconSize) RenderSystem.disableBlend() diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/icon/IconCache.kt b/src/main/kotlin/xyz/deathsgun/modmanager/icon/IconCache.kt index 76a7ae2..31b13f5 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/icon/IconCache.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/icon/IconCache.kt @@ -33,9 +33,6 @@ import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse import java.nio.file.Files -import java.nio.file.Path -import java.util.stream.Collectors -import kotlin.io.path.name class IconCache { @@ -43,115 +40,75 @@ class IconCache { private val unknownIcon = Identifier("textures/misc/unknown_pack.png") private val loadingIcon = Identifier("modmanager", "textures/gui/loading.png") private val iconsDir = FabricLoader.getInstance().gameDir.resolve(".icons") - private val state: ArrayList - private val icons = HashMap() - private val downloading = ArrayList() + private val state = HashMap() private val http = HttpClient.newHttpClient() - init { - state = loadCache() - } - @OptIn(DelicateCoroutinesApi::class) - fun downloadIcon(mod: Mod) { - if (mod.iconUrl == null) { - state.add(mod.id) - icons[mod.id] = unknownIcon - return - } - if (downloading.contains(mod.id)) { - return - } - downloading.add(mod.id) - GlobalScope.launch { - download(mod) - } - } - - fun bindIconTexture(mod: Mod) { - if (!hasIcon(mod)) { - RenderSystem.setShaderTexture(0, loadingIcon) - downloadIcon(mod) - return - } - val icon = getIcon(mod) - if (icon == null) { - RenderSystem.setShaderTexture(0, loadingIcon) - downloadIcon(mod) - return + fun bindIcon(mod: Mod) { + val icon = when (this.state[mod.id] ?: IconState.NOT_FOUND) { + IconState.NOT_FOUND -> { + GlobalScope.launch { + downloadIcon(mod) + } + loadingIcon + } + IconState.DOWNLOADED -> { + readIcon(mod) + } + IconState.LOADED -> { + Identifier("modmanager", "mod_icons/${mod.id.lowercase()}") + } + IconState.DOWNLOADING -> loadingIcon + IconState.ERRORED -> unknownIcon } RenderSystem.setShaderTexture(0, icon) } - fun destroyAll() { - icons.values.forEach { - MinecraftClient.getInstance().textureManager.destroyTexture(it) - } - } - - private fun hasIcon(mod: Mod): Boolean { - return state.contains(mod.id) - } - - private fun getIcon(mod: Mod): Identifier? { - if (!hasIcon(mod)) { - return unknownIcon - } - if (icons.containsKey(mod.id)) { - return icons[mod.id]!! - } - val iconFile = iconsDir.resolve(mod.id) - if (Files.exists(iconFile)) { - return readIcon(mod.id, iconFile) + private fun readIcon(mod: Mod): Identifier { + return try { + val icon = Identifier("modmanager", "mod_icons/${mod.id.lowercase()}") + val image = NativeImage.read(Files.newInputStream(iconsDir.resolve(mod.id))) + MinecraftClient.getInstance().textureManager.registerTexture(icon, NativeImageBackedTexture(image)) + this.state[mod.id] = IconState.LOADED + icon + } catch (e: Exception) { + this.state[mod.id] = IconState.ERRORED + logger.error("Error while loading icon for {}: {}", mod.slug, e.message) + unknownIcon } - return null } - private fun download(mod: Mod) { + private fun downloadIcon(mod: Mod) { if (mod.iconUrl == null) { + state[mod.id] = IconState.ERRORED + return + } + val iconState = state[mod.id] ?: IconState.NOT_FOUND + if (iconState != IconState.NOT_FOUND) { return } + state[mod.id] = IconState.DOWNLOADING try { val request = HttpRequest.newBuilder(URI.create(mod.iconUrl)).GET() .setHeader("User-Agent", "ModMenu " + ModManager.getVersion()).build() val response = http.send(request, HttpResponse.BodyHandlers.ofByteArray()) if (response.statusCode() != 200) { - logger.error( - "Failed to download icon for {} ! received status code: {}", mod.slug, - response.statusCode() - ) - state.add(mod.id) - icons[mod.id] = unknownIcon - downloading.removeIf { it == mod.id } + state[mod.id] = IconState.ERRORED return } - val iconFile = iconsDir.resolve(mod.id) - Files.write(iconFile, response.body()) - readIcon(mod.id, iconFile) - state.add(mod.id) + Files.write(iconsDir.resolve(mod.id), response.body()) + state[mod.id] = IconState.DOWNLOADED } catch (e: Exception) { - logger.error(e) - state.add(mod.id) - icons[mod.id] = unknownIcon + state[mod.id] = IconState.ERRORED + logger.error("Error while downloading icon for {}: {}", mod.slug, e.message) } - downloading.removeIf { it == mod.id } } - private fun readIcon(modId: String, file: Path): Identifier { - val image = NativeImage.read(Files.newInputStream(file)) - val identifier = Identifier("modmanager", "icons/${modId.lowercase()}") - MinecraftClient.getInstance().textureManager.registerTexture(identifier, NativeImageBackedTexture(image)) - icons[modId] = identifier - return identifier + fun destroyAll() { } - private fun loadCache(): ArrayList { - return try { - ArrayList(Files.list(iconsDir).map { it.name }.collect(Collectors.toList())) - } catch (e: Exception) { - Files.createDirectories(iconsDir) - ArrayList() - } + private enum class IconState { + NOT_FOUND, DOWNLOADING, DOWNLOADED, LOADED, ERRORED } } \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt index c60e5f0..632fbcf 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt @@ -72,7 +72,7 @@ class Modrinth : IModProvider, IModUpdateProvider { } CategoriesResult.Success(this.categories) } catch (e: Exception) { - logger.error("Error while getting categories: ", e) + logger.error("Error while getting categories: ", e.message) CategoriesResult.Error(TranslatableText("modmanager.response.failedToParse"), e) } } @@ -139,7 +139,7 @@ class Modrinth : IModProvider, IModUpdateProvider { val result = json.decodeFromString(response.body()) ModsResult.Success(result.toList()) } catch (e: Exception) { - logger.error("Error while requesting mods", e) + logger.error("Error while requesting mods", e.message) ModsResult.Error(TranslatableText("modmanager.error.failedToParse")) } } @@ -156,7 +156,7 @@ class Modrinth : IModProvider, IModUpdateProvider { val response = try { this.http.send(request, HttpResponse.BodyHandlers.ofString()) } catch (e: Exception) { - logger.error("Error while getting mod", e) + logger.error("Error while getting mod", e.message) return ModResult.Error(TranslatableText("modmanager.error.network"), e) } if (response.statusCode() != 200) { @@ -198,7 +198,7 @@ class Modrinth : IModProvider, IModUpdateProvider { val response = try { this.http.send(request, HttpResponse.BodyHandlers.ofString()) } catch (e: Exception) { - logger.error("Error while getting mod", e) + logger.error("Error while getting mod", e.message) return VersionResult.Error(TranslatableText("modmanager.error.network"), e) } if (response.statusCode() != 200) { From 9ebe666283cf482bee6a7385ad804672c118d081 Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Wed, 1 Sep 2021 18:48:33 +0200 Subject: [PATCH 12/25] Added paging Signed-off-by: DeathsGun --- .../modmanager/api/gui/list/ListWidget.kt | 4 + .../modmanager/gui/ModsOverviewScreen.kt | 88 +++++++++++++++++-- .../modmanager/gui/widget/ModListWidget.kt | 1 + 3 files changed, 87 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/ListWidget.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/ListWidget.kt index a2a3468..1386b03 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/ListWidget.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/ListWidget.kt @@ -136,6 +136,10 @@ abstract class ListWidget>( ) children()[index] else null } + fun getElementCount(): Int { + return super.getEntryCount() + } + abstract class Entry>(open val list: ListWidget, val id: String) : AlwaysSelectedEntryListWidget.Entry() { @Suppress("UNCHECKED_CAST") diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt index f027cf2..dfcaa8f 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import net.minecraft.client.gui.Element import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.widget.ButtonWidget import net.minecraft.client.gui.widget.TextFieldWidget import net.minecraft.client.util.math.MatrixStack import net.minecraft.text.TranslatableText @@ -28,6 +29,7 @@ import org.lwjgl.glfw.GLFW import xyz.deathsgun.modmanager.ModManager import xyz.deathsgun.modmanager.api.gui.list.IListScreen import xyz.deathsgun.modmanager.api.http.ModsResult +import xyz.deathsgun.modmanager.api.mod.Category import xyz.deathsgun.modmanager.api.provider.Sorting import xyz.deathsgun.modmanager.gui.widget.ErrorWidget import xyz.deathsgun.modmanager.gui.widget.ModListEntry @@ -38,9 +40,15 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab private var query: String = "" private var selectedMod: ModListEntry? = null - private lateinit var errorWidget: ErrorWidget + private var page: Int = 0 + private var limit: Int = 20 + private var scrollPercentage: Double = 0.0 + private var category: Category? = null private lateinit var searchField: TextFieldWidget + private lateinit var errorWidget: ErrorWidget private lateinit var modList: ModListWidget + private lateinit var nextPage: ButtonWidget + private lateinit var previousPage: ButtonWidget @OptIn(DelicateCoroutinesApi::class) override fun init() { @@ -50,18 +58,51 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab textRenderer, 10, 10, - 100, + 160, 20, TranslatableText("modmanager.search") ) ) searchField.setChangedListener { this.query = it } - errorWidget = ErrorWidget(client!!, 10, 35, width - 130, height - 150) - modList = addSelectableChild(ModListWidget(client!!, width - 130, height, 35, height - 50, 36, this)) - modList.setLeftPos(10) + errorWidget = ErrorWidget(client!!, 110, 35, width - 20, height - 150) + modList = addSelectableChild(ModListWidget(client!!, width - 10 - 110, height, 35, height - 30, 36, this)) + modList.setLeftPos(110) + + val buttonWidth = (width - 110 - 10 - 20) / 2 + + previousPage = addDrawableChild( + ButtonWidget( + 110, + height - 25, + buttonWidth, + 20, + TranslatableText("modmanager.page.previous") + ) { showPreviousPage() }) + nextPage = addDrawableChild( + ButtonWidget( + 110 + buttonWidth + 20, + height - 25, + buttonWidth, + 20, + TranslatableText("modmanager.page.next") + ) { showNextPage() }) + GlobalScope.launch { val provider = ModManager.modManager.getSelectedProvider() ?: return@launch - when (val result = provider.getMods(Sorting.RELEVANCE, 0, 20)) { + if (query.isNotEmpty()) { + when (val result = provider.search(query, page, limit)) { + is ModsResult.Error -> { + errorWidget.error = result.text + } + is ModsResult.Success -> { + errorWidget.error = null + modList.setMods(result.mods) + modList.scrollAmount = scrollPercentage + } + } + return@launch + } + when (val result = provider.getMods(Sorting.RELEVANCE, page, limit)) { is ModsResult.Error -> { errorWidget.error = result.text } @@ -75,6 +116,38 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab //TODO: Paging } + private fun showNextPage() { + this.page++ + if (this.category == null) { + showByRelevanceMods() + return + } + } + + private fun showPreviousPage() { + this.page-- + if (this.page < 0) { + page = 0 + } + if (this.category == null) { + showByRelevanceMods() + return + } + } + + private fun showByRelevanceMods() { + val provider = ModManager.modManager.getSelectedProvider() ?: return + when (val result = provider.getMods(Sorting.RELEVANCE, page, limit)) { + is ModsResult.Error -> { + errorWidget.error = result.text + } + is ModsResult.Success -> { + errorWidget.error = null + modList.setMods(result.mods) + } + } + } + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { if (this.searchField.isFocused && keyCode == GLFW.GLFW_KEY_ENTER) { val provider = ModManager.modManager.getSelectedProvider() ?: return true @@ -116,7 +189,10 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab override fun tick() { super.tick() + this.scrollPercentage = modList.scrollAmount this.searchField.tick() + this.previousPage.active = page > 0 + this.nextPage.active = this.modList.getElementCount() >= limit } override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt index 4877d59..7b291e2 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt @@ -36,6 +36,7 @@ class ModListWidget( } fun setMods(mods: List) { + this.clearEntries() mods.forEach { this.addEntry(ModListEntry(client, this, it)) } From e77056f2bb1c0a4c8f704f1fb2efec7394cfaaa0 Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Thu, 2 Sep 2021 21:39:12 +0200 Subject: [PATCH 13/25] Use own CategoryListWidget and only check updates when a jar file exits for it Signed-off-by: DeathsGun --- .../modmanager/mixin/TitleScreenMixin.java | 22 +++- .../xyz/deathsgun/modmanager/ModManager.kt | 7 +- .../modmanager/api/gui/list/IListScreen.kt | 18 ++- .../xyz/deathsgun/modmanager/api/mod/Mod.kt | 2 +- .../modmanager/api/provider/IModProvider.kt | 4 +- .../modmanager/gui/ModsOverviewScreen.kt | 109 +++++++++++++----- .../gui/widget/CategoryListEntry.kt | 56 +++++++++ .../gui/widget/CategoryListWidget.kt | 44 +++++++ .../modmanager/models/FabricMetadata.kt | 24 ++++ .../modmanager/providers/modrinth/Modrinth.kt | 10 +- .../providers/modrinth/models/DetailedMod.kt | 5 +- .../xyz/deathsgun/modmanager/update/Update.kt | 21 ++++ .../modmanager/update/UpdateManager.kt | 67 ++++++++++- .../providers/modrinth/ModrinthTest.kt | 10 +- 14 files changed, 342 insertions(+), 57 deletions(-) create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListEntry.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/models/FabricMetadata.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/update/Update.kt diff --git a/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java b/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java index 3e02144..8ed1089 100644 --- a/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java +++ b/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java @@ -1,3 +1,19 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package xyz.deathsgun.modmanager.mixin; import net.minecraft.client.gui.screen.Screen; @@ -11,9 +27,9 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import xyz.deathsgun.modmanager.ModManager; -import xyz.deathsgun.modmanager.api.mod.Version; +import xyz.deathsgun.modmanager.update.Update; -import java.util.Map; +import java.util.ArrayList; import java.util.Objects; @Mixin(TitleScreen.class) @@ -31,7 +47,7 @@ public void onRender(MatrixStack matrices, int mouseX, int mouseY, float delta, return; } hasRun = true; - Map updates = ModManager.modManager.getUpdate().getUpdates(); + ArrayList updates = ModManager.modManager.getUpdate().getUpdates(); if (updates.isEmpty()) { return; } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt index 147a39a..940a66d 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt @@ -46,11 +46,8 @@ class ModManager : ClientModInitializer { @JvmStatic fun getVersion(): String { - return try { - FabricLoader.getInstance().allMods.first { it.metadata.id.equals("modmanager") }.metadata.version.friendlyString - } catch (ignored: NoSuchElementException) { - "1.1.0+1.17.1-alpha" - } + return FabricLoader.getInstance().allMods.find { it.metadata.id.equals("modmanager") } + ?.metadata?.version?.friendlyString ?: "1.1.0+1.17.1-alpha" } @JvmStatic diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/IListScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/IListScreen.kt index dbd1754..b39528a 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/IListScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/IListScreen.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package xyz.deathsgun.modmanager.api.gui.list import net.minecraft.client.gui.Element @@ -8,6 +24,4 @@ interface IListScreen { fun updateSelectedEntry(widget: Any, entry: E?) - fun getEntry(widget: Any): E? - } \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt index ac56d11..c6f0fa2 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt @@ -19,7 +19,7 @@ package xyz.deathsgun.modmanager.api.mod data class Mod( val id: String, val slug: String, - val author: String, + val author: String?, val name: String, val shortDescription: String, val iconUrl: String?, diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt index d12e87c..9910158 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt @@ -73,8 +73,8 @@ interface IModProvider { /** * Returns a more detailed representation of the mod * - * @param mod the [Mod] which is used to receive + * @param id the [Mod] id which is used to receive * @return a more detailed representation of [Mod] */ - fun getMod(mod: Mod): ModResult + fun getMod(id: String): ModResult } \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt index dfcaa8f..7f056a5 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt @@ -28,25 +28,24 @@ import net.minecraft.text.TranslatableText import org.lwjgl.glfw.GLFW import xyz.deathsgun.modmanager.ModManager import xyz.deathsgun.modmanager.api.gui.list.IListScreen +import xyz.deathsgun.modmanager.api.http.CategoriesResult import xyz.deathsgun.modmanager.api.http.ModsResult -import xyz.deathsgun.modmanager.api.mod.Category import xyz.deathsgun.modmanager.api.provider.Sorting -import xyz.deathsgun.modmanager.gui.widget.ErrorWidget -import xyz.deathsgun.modmanager.gui.widget.ModListEntry -import xyz.deathsgun.modmanager.gui.widget.ModListWidget +import xyz.deathsgun.modmanager.gui.widget.* class ModsOverviewScreen(private val previousScreen: Screen) : Screen(TranslatableText("modmanager.title.overview")), IListScreen { private var query: String = "" private var selectedMod: ModListEntry? = null + private var selectedCategory: CategoryListEntry? = null private var page: Int = 0 private var limit: Int = 20 private var scrollPercentage: Double = 0.0 - private var category: Category? = null private lateinit var searchField: TextFieldWidget private lateinit var errorWidget: ErrorWidget private lateinit var modList: ModListWidget + private lateinit var categoryList: CategoryListWidget private lateinit var nextPage: ButtonWidget private lateinit var previousPage: ButtonWidget @@ -64,15 +63,31 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab ) ) searchField.setChangedListener { this.query = it } - errorWidget = ErrorWidget(client!!, 110, 35, width - 20, height - 150) - modList = addSelectableChild(ModListWidget(client!!, width - 10 - 110, height, 35, height - 30, 36, this)) - modList.setLeftPos(110) - val buttonWidth = (width - 110 - 10 - 20) / 2 + categoryList = addSelectableChild( + CategoryListWidget( + client!!, + 100, + height, + 35, + height - 30, + client!!.textRenderer.fontHeight + 4, + this + ) + ) + categoryList.setLeftPos(10) + + errorWidget = ErrorWidget(client!!, 115, 35, width - 20, height - 150) + + modList = addSelectableChild(ModListWidget(client!!, width - 10 - 115, height, 35, height - 30, 36, this)) + modList.setLeftPos(115) + + + val buttonWidth = (width - 115 - 10 - 20) / 2 previousPage = addDrawableChild( ButtonWidget( - 110, + 115, height - 25, buttonWidth, 20, @@ -80,7 +95,7 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab ) { showPreviousPage() }) nextPage = addDrawableChild( ButtonWidget( - 110 + buttonWidth + 20, + 115 + buttonWidth + 20, height - 25, buttonWidth, 20, @@ -89,8 +104,18 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab GlobalScope.launch { val provider = ModManager.modManager.getSelectedProvider() ?: return@launch - if (query.isNotEmpty()) { - when (val result = provider.search(query, page, limit)) { + when (val result = provider.getCategories()) { + is CategoriesResult.Error -> { + errorWidget.error = result.text + } + is CategoriesResult.Success -> { + errorWidget.error = null + categoryList.setCategories(result.categories) + modList.scrollAmount = scrollPercentage + } + } + if (selectedCategory != null) { + when (val result = provider.getMods(selectedCategory!!.category, page, limit)) { is ModsResult.Error -> { errorWidget.error = result.text } @@ -102,26 +127,34 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab } return@launch } - when (val result = provider.getMods(Sorting.RELEVANCE, page, limit)) { - is ModsResult.Error -> { - errorWidget.error = result.text - } - is ModsResult.Success -> { - errorWidget.error = null - modList.setMods(result.mods) + if (query.isNotEmpty()) { + when (val result = provider.search(query, page, limit)) { + is ModsResult.Error -> { + errorWidget.error = result.text + } + is ModsResult.Success -> { + errorWidget.error = null + modList.setMods(result.mods) + modList.scrollAmount = scrollPercentage + } } + return@launch } + showModsByRelevance() } + modList.init() + categoryList.init() //TODO: Sorting selector //TODO: Paging } private fun showNextPage() { this.page++ - if (this.category == null) { - showByRelevanceMods() + if (selectedCategory == null) { + showModsByRelevance() return } + showModsByCategory() } private fun showPreviousPage() { @@ -129,13 +162,14 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab if (this.page < 0) { page = 0 } - if (this.category == null) { - showByRelevanceMods() + if (selectedCategory == null) { + showModsByRelevance() return } + showModsByCategory() } - private fun showByRelevanceMods() { + private fun showModsByRelevance() { val provider = ModManager.modManager.getSelectedProvider() ?: return when (val result = provider.getMods(Sorting.RELEVANCE, page, limit)) { is ModsResult.Error -> { @@ -148,6 +182,19 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab } } + private fun showModsByCategory() { + val provider = ModManager.modManager.getSelectedProvider() ?: return + when (val result = provider.getMods(selectedCategory!!.category, page, limit)) { + is ModsResult.Error -> { + errorWidget.error = result.text + } + is ModsResult.Success -> { + errorWidget.error = null + modList.setMods(result.mods) + } + } + } + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { if (this.searchField.isFocused && keyCode == GLFW.GLFW_KEY_ENTER) { val provider = ModManager.modManager.getSelectedProvider() ?: return true @@ -181,10 +228,13 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab selectedMod = entry as ModListEntry return } - } - - override fun getEntry(widget: Any): E? { - return null + if (widget is CategoryListWidget) { + if (entry == null) { + return + } + selectedCategory = entry as CategoryListEntry + showModsByCategory() + } } override fun tick() { @@ -201,6 +251,7 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab if (this.errorWidget.error == null) { this.modList.render(matrices, mouseX, mouseY, delta) } + this.categoryList.render(matrices, mouseX, mouseY, delta) this.searchField.render(matrices, mouseX, mouseY, delta) super.render(matrices, mouseX, mouseY, delta) } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListEntry.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListEntry.kt new file mode 100644 index 0000000..5681a5b --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListEntry.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.gui.widget + +import net.minecraft.client.MinecraftClient +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.text.OrderedText +import net.minecraft.text.Text +import net.minecraft.util.Language +import xyz.deathsgun.modmanager.api.gui.list.ListWidget +import xyz.deathsgun.modmanager.api.mod.Category + + +class CategoryListEntry(list: ListWidget, val category: Category) : + ListWidget.Entry(list, category.id) { + + override fun render( + matrices: MatrixStack?, + index: Int, + y: Int, + x: Int, + entryWidth: Int, + entryHeight: Int, + mouseX: Int, + mouseY: Int, + hovered: Boolean, + tickDelta: Float + ) { + val font = MinecraftClient.getInstance().textRenderer + var text: Text = category.text + if (list.isSelectedEntry(this)) { + text = text.getWithStyle(text.style.withBold(true))[0] + } + val trimmedText: OrderedText = Language.getInstance().reorder(font.trimToWidth(text, entryWidth - 10)) + font.draw(matrices, trimmedText, (x + 3).toFloat(), (y + 1).toFloat(), 0xFFFFFF) + } + + override fun getNarration(): Text { + return category.text + } + +} diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.kt new file mode 100644 index 0000000..222e49b --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.gui.widget + +import net.minecraft.client.MinecraftClient +import xyz.deathsgun.modmanager.api.gui.list.IListScreen +import xyz.deathsgun.modmanager.api.gui.list.ListWidget +import xyz.deathsgun.modmanager.api.mod.Category + +class CategoryListWidget( + client: MinecraftClient, + width: Int, + height: Int, + top: Int, + bottom: Int, + itemHeight: Int, + parent: IListScreen +) : ListWidget(client, width, height, top, bottom, itemHeight, parent) { + + override fun init() { + + } + + fun setCategories(categories: List) { + clearEntries() + categories.forEach { + addEntry(CategoryListEntry(this, it)) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/models/FabricMetadata.kt b/src/main/kotlin/xyz/deathsgun/modmanager/models/FabricMetadata.kt new file mode 100644 index 0000000..2500583 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/models/FabricMetadata.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.models + +import kotlinx.serialization.Serializable + +@Serializable +data class FabricMetadata( + val id: String +) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt index 632fbcf..ba8ea2e 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt @@ -148,8 +148,8 @@ class Modrinth : IModProvider, IModUpdateProvider { this.ignoreUnknownKeys = true } - override fun getMod(mod: Mod): ModResult { - val id = mod.id.replaceFirst("local-", "") + override fun getMod(id: String): ModResult { + id.replaceFirst("local-", "") val request = HttpRequest.newBuilder().GET() .setHeader("User-Agent", "ModManager " + ModManager.getVersion()) .uri(URI.create("${baseUri}/api/v1/mod/${id}")).build() @@ -177,10 +177,10 @@ class Modrinth : IModProvider, IModUpdateProvider { Mod( id = result.id.replaceFirst("local-", ""), slug = result.slug, - author = mod.author, + author = null, name = result.title, - shortDescription = mod.shortDescription, - iconUrl = mod.iconUrl, + shortDescription = result.description, + iconUrl = result.iconUrl, description = result.body, license = result.license.name, categories = categoriesList diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/DetailedMod.kt b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/DetailedMod.kt index 6c3abf9..13bd25f 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/DetailedMod.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/DetailedMod.kt @@ -16,6 +16,7 @@ package xyz.deathsgun.modmanager.providers.modrinth.models +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable @@ -26,7 +27,9 @@ data class DetailedMod( val description: String, val body: String, val license: License, - val categories: List + val categories: List, + @SerialName("icon_url") + val iconUrl: String? ) { @Serializable data class License(val name: String) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/update/Update.kt b/src/main/kotlin/xyz/deathsgun/modmanager/update/Update.kt new file mode 100644 index 0000000..f42023d --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/update/Update.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.update + +import xyz.deathsgun.modmanager.api.mod.Version + +data class Update(val modId: String, val fabricId: String, val version: Version) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt index 227cb9c..60dd1b0 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt @@ -1,8 +1,27 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package xyz.deathsgun.modmanager.update import com.terraformersmc.modmenu.util.mod.fabric.CustomValueUtil import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json import net.fabricmc.loader.api.FabricLoader import net.fabricmc.loader.api.SemanticVersion import net.fabricmc.loader.api.metadata.ModMetadata @@ -11,21 +30,32 @@ import org.apache.logging.log4j.LogManager import xyz.deathsgun.modmanager.ModManager import xyz.deathsgun.modmanager.api.http.ModsResult import xyz.deathsgun.modmanager.api.http.VersionResult +import xyz.deathsgun.modmanager.api.mod.Mod import xyz.deathsgun.modmanager.api.mod.Version import xyz.deathsgun.modmanager.api.provider.IModUpdateProvider +import xyz.deathsgun.modmanager.models.FabricMetadata import xyz.deathsgun.modmanager.state.ModState +import java.nio.file.Files +import java.nio.file.Path +import java.util.stream.Collectors +import java.util.zip.ZipFile +import kotlin.io.path.name class UpdateManager { private val logger = LogManager.getLogger("UpdateCheck") private val blockedIds = arrayOf("java", "minecraft") - val updates = HashMap() + val updates = ArrayList() suspend fun checkUpdates() = coroutineScope { val mods = getCheckableMods() mods.forEach { metadata -> launch { + if (findJarByModContainer(metadata) == null) { + logger.info("Skipping update for {} because it has no jar in mods", metadata.id) + return@launch + } val configIds = getIdBy(metadata) if (configIds == null) { logger.info("Searching for updates for {} using fallback method", metadata.id) @@ -60,7 +90,7 @@ class UpdateManager { } logger.info("Update for {} found [{} -> {}]", metadata.id, metadata.version.friendlyString, version.version) ModManager.modManager.setModState(metadata.id, metadata.id, ModState.OUTDATED) - this.updates[metadata.id] = version + this.updates.add(Update(metadata.id, metadata.id, version)) return } @@ -94,7 +124,7 @@ class UpdateManager { } logger.info("Update for {} found [{} -> {}]", metadata.id, metadata.version.friendlyString, version.version) ModManager.modManager.setModState(metadata.id, mod.id, ModState.OUTDATED) - this.updates[metadata.id] = version + this.updates.add(Update(mod.id, metadata.id, version)) } private fun checkForUpdates(metadata: ModMetadata, ids: Map) { @@ -130,7 +160,11 @@ class UpdateManager { } logger.info("Update for {} found [{} -> {}]", metadata.id, metadata.version.friendlyString, version.version) ModManager.modManager.setModState(metadata.id, id, ModState.OUTDATED) - this.updates[metadata.id] = version + this.updates.add(Update(id, metadata.id, version)) + } + + fun getUpdateForMod(mod: Mod): Update? { + return this.updates.find { it.modId == mod.id || it.fabricId == mod.slug } } private fun findLatestCompatible(installedVersion: String, versions: List): Version? { @@ -157,6 +191,31 @@ class UpdateManager { return latest } + private val json = Json { + ignoreUnknownKeys = true + } + + @OptIn(ExperimentalSerializationApi::class) + fun findJarByModContainer(container: ModMetadata): Path? { + val jars = Files.list(FabricLoader.getInstance().gameDir.resolve("mods")) + .filter { it.name.endsWith(".jar") }.collect(Collectors.toList()) + return try { + for (jar in jars) { + val jarFile = ZipFile(jar.toFile()) + val fabricEntry = jarFile.getEntry("fabric.mod.json") + val data = jarFile.getInputStream(fabricEntry).bufferedReader().use { it.readText() } + val meta = json.decodeFromString(data) + jarFile.close() + if (meta.id == container.id) { + return jar + } + } + null + } catch (e: Exception) { + null + } + } + private fun getIdBy(metadata: ModMetadata): Map? { if (!metadata.containsCustomValue("modmanager")) { return null diff --git a/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt b/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt index 373c0f6..255610b 100644 --- a/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt +++ b/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt @@ -95,7 +95,7 @@ internal class ModrinthTest { assertTrue(it.id.isNotEmpty()) assertTrue(it.slug.isNotEmpty()) assertTrue(it.name.isNotEmpty()) - assertTrue(it.author.isNotEmpty()) + assertNotNull(it.author) assertNotNull(it.iconUrl) assertTrue(it.shortDescription.isNotEmpty()) assertTrue(it.categories.isNotEmpty()) @@ -115,7 +115,7 @@ internal class ModrinthTest { } fail(testMod.text.key) } - val result = modrinth.getMod((testMod as ModsResult.Success).mods[0]) + val result = modrinth.getMod((testMod as ModsResult.Success).mods[0].id) if (result is ModResult.Error) { result.cause?.let { fail(result.text.key, it) @@ -126,13 +126,13 @@ internal class ModrinthTest { assertTrue(mod.id.isNotEmpty()) assertTrue(mod.slug.isNotEmpty()) assertTrue(mod.name.isNotEmpty()) - assertTrue(mod.author.isNotEmpty()) + assertNull(mod.author) assertNotNull(mod.iconUrl) assertTrue(mod.shortDescription.isNotEmpty()) assertTrue(mod.categories.isNotEmpty()) - assertNotEquals(mod.description, null) + assertNotNull(mod.description) assertTrue(mod.description!!.isNotEmpty()) - assertNotEquals(mod.license, null) + assertNotNull(mod.license) assertTrue(mod.license!!.isNotEmpty()) } From 4f24a858b8d298a98ce34947c020e60e232091aa Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Fri, 3 Sep 2021 10:18:47 +0200 Subject: [PATCH 14/25] Show updatable mods Signed-off-by: DeathsGun --- .../modmanager/mixin/TitleScreenMixin.java | 6 ++---- .../xyz/deathsgun/modmanager/ModManager.kt | 3 +++ .../api/provider/IModUpdateProvider.kt | 2 ++ .../modmanager/gui/ModsOverviewScreen.kt | 19 ++++++++++++++++- .../gui/widget/CategoryListWidget.kt | 11 ++++++++-- .../modmanager/gui/widget/ModListWidget.kt | 8 +++++++ .../xyz/deathsgun/modmanager/update/Update.kt | 3 ++- .../modmanager/update/UpdateManager.kt | 21 +++++++++++++++---- .../assets/modmanager/lang/en_us.json | 8 +++---- .../assets/modmanager/lang/ko_kr.json | 2 +- .../assets/modmanager/lang/ru_ru.json | 2 +- .../assets/modmanager/lang/tr_tr.json | 2 +- .../assets/modmanager/lang/zh_cn.json | 8 +++---- src/main/resources/fabric.mod.json | 2 +- 14 files changed, 73 insertions(+), 24 deletions(-) diff --git a/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java b/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java index 8ed1089..24bbaa8 100644 --- a/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java +++ b/src/main/java/xyz/deathsgun/modmanager/mixin/TitleScreenMixin.java @@ -35,18 +35,16 @@ @Mixin(TitleScreen.class) public class TitleScreenMixin extends Screen { - private boolean hasRun = false; - protected TitleScreenMixin(Text title) { super(title); } @Inject(at = @At("TAIL"), method = "render") public void onRender(MatrixStack matrices, int mouseX, int mouseY, float delta, CallbackInfo ci) { - if (hasRun) { + if (ModManager.shownUpdateNotification) { return; } - hasRun = true; + ModManager.shownUpdateNotification = true; ArrayList updates = ModManager.modManager.getUpdate().getUpdates(); if (updates.isEmpty()) { return; diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt index 940a66d..ebc01a5 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt @@ -41,6 +41,9 @@ class ModManager : ClientModInitializer { val updateProvider: HashMap = HashMap() companion object { + @JvmField + var shownUpdateNotification: Boolean = false + @JvmStatic lateinit var modManager: ModManager diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModUpdateProvider.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModUpdateProvider.kt index 6796e0d..955bfda 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModUpdateProvider.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModUpdateProvider.kt @@ -22,4 +22,6 @@ interface IModUpdateProvider { fun getVersionsForMod(id: String): VersionResult + fun getName(): String + } \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt index 7f056a5..34af9d5 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt @@ -30,6 +30,7 @@ import xyz.deathsgun.modmanager.ModManager import xyz.deathsgun.modmanager.api.gui.list.IListScreen import xyz.deathsgun.modmanager.api.http.CategoriesResult import xyz.deathsgun.modmanager.api.http.ModsResult +import xyz.deathsgun.modmanager.api.mod.Category import xyz.deathsgun.modmanager.api.provider.Sorting import xyz.deathsgun.modmanager.gui.widget.* @@ -110,7 +111,11 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab } is CategoriesResult.Success -> { errorWidget.error = null - categoryList.setCategories(result.categories) + categoryList.clear() + if (ModManager.modManager.update.updates.isNotEmpty()) { + categoryList.add(Category("updatable", TranslatableText("modmanager.category.updatable"))) + } + categoryList.addCategories(result.categories) modList.scrollAmount = scrollPercentage } } @@ -182,8 +187,19 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab } } + @OptIn(DelicateCoroutinesApi::class) private fun showModsByCategory() { + selectedCategory ?: return val provider = ModManager.modManager.getSelectedProvider() ?: return + if (selectedCategory!!.id == "updatable") { + GlobalScope.launch { + modList.clear() + ModManager.modManager.update.updates.forEach { + modList.add(it.mod) + } + } + return + } when (val result = provider.getMods(selectedCategory!!.category, page, limit)) { is ModsResult.Error -> { errorWidget.error = result.text @@ -232,6 +248,7 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab if (entry == null) { return } + page = 0 selectedCategory = entry as CategoryListEntry showModsByCategory() } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.kt index 222e49b..95c3fe4 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.kt @@ -35,10 +35,17 @@ class CategoryListWidget( } - fun setCategories(categories: List) { - clearEntries() + fun addCategories(categories: List) { categories.forEach { addEntry(CategoryListEntry(this, it)) } } + + fun clear() { + clearEntries() + } + + fun add(category: Category) { + addEntry(CategoryListEntry(this, category)) + } } \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt index 7b291e2..98cc15b 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ModListWidget.kt @@ -42,4 +42,12 @@ class ModListWidget( } } + fun clear() { + this.clearEntries() + } + + fun add(mod: Mod) { + this.addEntry(ModListEntry(client, this, mod)) + } + } \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/update/Update.kt b/src/main/kotlin/xyz/deathsgun/modmanager/update/Update.kt index f42023d..d092f85 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/update/Update.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/update/Update.kt @@ -16,6 +16,7 @@ package xyz.deathsgun.modmanager.update +import xyz.deathsgun.modmanager.api.mod.Mod import xyz.deathsgun.modmanager.api.mod.Version -data class Update(val modId: String, val fabricId: String, val version: Version) +data class Update(val mod: Mod, val fabricId: String, val version: Version) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt index 60dd1b0..4cc81e0 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt @@ -28,6 +28,7 @@ import net.fabricmc.loader.api.metadata.ModMetadata import net.fabricmc.loader.util.version.VersionDeserializer import org.apache.logging.log4j.LogManager import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.api.http.ModResult import xyz.deathsgun.modmanager.api.http.ModsResult import xyz.deathsgun.modmanager.api.http.VersionResult import xyz.deathsgun.modmanager.api.mod.Mod @@ -90,7 +91,11 @@ class UpdateManager { } logger.info("Update for {} found [{} -> {}]", metadata.id, metadata.version.friendlyString, version.version) ModManager.modManager.setModState(metadata.id, metadata.id, ModState.OUTDATED) - this.updates.add(Update(metadata.id, metadata.id, version)) + when (val modResult = provider.getMod(metadata.id)) { + is ModResult.Success -> { + this.updates.add(Update(modResult.mod, metadata.id, version)) + } + } return } @@ -124,7 +129,11 @@ class UpdateManager { } logger.info("Update for {} found [{} -> {}]", metadata.id, metadata.version.friendlyString, version.version) ModManager.modManager.setModState(metadata.id, mod.id, ModState.OUTDATED) - this.updates.add(Update(mod.id, metadata.id, version)) + when (val modResult = provider.getMod(mod.id)) { + is ModResult.Success -> { + this.updates.add(Update(modResult.mod, metadata.id, version)) + } + } } private fun checkForUpdates(metadata: ModMetadata, ids: Map) { @@ -160,11 +169,15 @@ class UpdateManager { } logger.info("Update for {} found [{} -> {}]", metadata.id, metadata.version.friendlyString, version.version) ModManager.modManager.setModState(metadata.id, id, ModState.OUTDATED) - this.updates.add(Update(id, metadata.id, version)) + when (val modResult = ModManager.modManager.provider[provider.getName()]?.getMod(id)) { + is ModResult.Success -> { + this.updates.add(Update(modResult.mod, metadata.id, version)) + } + } } fun getUpdateForMod(mod: Mod): Update? { - return this.updates.find { it.modId == mod.id || it.fabricId == mod.slug } + return this.updates.find { it.mod.id == mod.id || it.fabricId == mod.slug } } private fun findLatestCompatible(installedVersion: String, versions: List): Version? { diff --git a/src/main/resources/assets/modmanager/lang/en_us.json b/src/main/resources/assets/modmanager/lang/en_us.json index 2fb744f..8446043 100644 --- a/src/main/resources/assets/modmanager/lang/en_us.json +++ b/src/main/resources/assets/modmanager/lang/en_us.json @@ -1,8 +1,8 @@ { - "modmanager.badge.installed": "Installed", - "modmanager.badge.outdated": "Outdated", - "modmanager.categories": "Categories", - "modmanager.categories.updatable": "Updatable Mods", + "modmanager.badge.installed": "Installed", + "modmanager.badge.outdated": "Outdated", + "modmanager.categories": "Categories", + "modmanager.category.updatable": "Updatable Mods", "modmanager.category.technology": "Technology", "modmanager.category.adventure": "Adventure", "modmanager.category.magic": "Magic", diff --git a/src/main/resources/assets/modmanager/lang/ko_kr.json b/src/main/resources/assets/modmanager/lang/ko_kr.json index 4b1037a..55e5acc 100644 --- a/src/main/resources/assets/modmanager/lang/ko_kr.json +++ b/src/main/resources/assets/modmanager/lang/ko_kr.json @@ -2,7 +2,7 @@ "modmanager.badge.installed": "설치됨", "modmanager.badge.outdated": "업데이트 필요", "modmanager.categories": "카테고리", - "modmanager.categories.updatable": "업데이트 가능", + "modmanager.category.updatable": "업데이트 가능", "modmanager.category.technology": "기술", "modmanager.category.adventure": "모험", "modmanager.category.magic": "마법", diff --git a/src/main/resources/assets/modmanager/lang/ru_ru.json b/src/main/resources/assets/modmanager/lang/ru_ru.json index 0b33c6e..6338450 100644 --- a/src/main/resources/assets/modmanager/lang/ru_ru.json +++ b/src/main/resources/assets/modmanager/lang/ru_ru.json @@ -2,7 +2,7 @@ "modmanager.badge.installed": "Установлен", "modmanager.badge.outdated": "Устарел", "modmanager.categories": "Категории", - "modmanager.categories.updatable": "Доступны обновления", + "modmanager.category.updatable": "Доступны обновления", "modmanager.category.technology": "Технологии", "modmanager.category.adventure": "Приключения", "modmanager.category.magic": "Магия", diff --git a/src/main/resources/assets/modmanager/lang/tr_tr.json b/src/main/resources/assets/modmanager/lang/tr_tr.json index 7776bce..1b668ff 100644 --- a/src/main/resources/assets/modmanager/lang/tr_tr.json +++ b/src/main/resources/assets/modmanager/lang/tr_tr.json @@ -2,7 +2,7 @@ "modmanager.badge.installed": "Kurulu", "modmanager.badge.outdated": "Eski", "modmanager.categories": "Kategoriler", - "modmanager.categories.updatable": "Güncellenebilir Modlar", + "modmanager.category.updatable": "Güncellenebilir Modlar", "modmanager.category.technology": "Teknoloji", "modmanager.category.adventure": "Macera", "modmanager.category.magic": "Sihir", diff --git a/src/main/resources/assets/modmanager/lang/zh_cn.json b/src/main/resources/assets/modmanager/lang/zh_cn.json index 072f031..65915c4 100644 --- a/src/main/resources/assets/modmanager/lang/zh_cn.json +++ b/src/main/resources/assets/modmanager/lang/zh_cn.json @@ -1,8 +1,8 @@ { - "modmanager.badge.installed": "已安装", - "modmanager.badge.outdated": "可更新", - "modmanager.categories": "分类", - "modmanager.categories.updatable": "可更新", + "modmanager.badge.installed": "已安装", + "modmanager.badge.outdated": "可更新", + "modmanager.categories": "分类", + "modmanager.category.updatable": "可更新", "modmanager.category.technology": "科技", "modmanager.category.adventure": "冒险", "modmanager.category.magic": "魔法", diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index f005906..6655020 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -25,7 +25,7 @@ }, "custom": { "modmanager": { - "Modrinth": "6kq7BzRK" + "modrinth": "6kq7BzRK" } }, "mixins": [ From 0b4567fce42bea1fd88f25c67004646d22fa1a37 Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Fri, 3 Sep 2021 14:14:35 +0200 Subject: [PATCH 15/25] View updates first Signed-off-by: DeathsGun --- .../deathsgun/modmanager/gui/ModsOverviewScreen.kt | 14 +++----------- .../modmanager/gui/widget/CategoryListWidget.kt | 4 ++++ 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt index 34af9d5..6503b3b 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt @@ -120,16 +120,8 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab } } if (selectedCategory != null) { - when (val result = provider.getMods(selectedCategory!!.category, page, limit)) { - is ModsResult.Error -> { - errorWidget.error = result.text - } - is ModsResult.Success -> { - errorWidget.error = null - modList.setMods(result.mods) - modList.scrollAmount = scrollPercentage - } - } + showModsByCategory() + modList.scrollAmount = scrollPercentage return@launch } if (query.isNotEmpty()) { @@ -145,7 +137,7 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab } return@launch } - showModsByRelevance() + categoryList.setSelectedByIndex(0) } modList.init() categoryList.init() diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.kt index 95c3fe4..0f0b660 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/CategoryListWidget.kt @@ -41,6 +41,10 @@ class CategoryListWidget( } } + fun setSelectedByIndex(index: Int) { + setSelected(getEntry(index)) + } + fun clear() { clearEntries() } From 79122c8f47aa7c59133d7b0a5dc27535bb9ef915 Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Mon, 6 Sep 2021 11:11:49 +0200 Subject: [PATCH 16/25] Update test.yml --- .github/workflows/test.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 79ec88a..c7013f0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,10 +22,3 @@ jobs: if: always() with: files: build/test-results/**/*.xml - - name: build - run: ./gradlew build - - name: capture build artifacts - uses: actions/upload-artifact@v2 - with: - name: Artifacts - path: build/libs/ From e949c69ef79ec10567aedba9511c2ceae19d4b12 Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Mon, 6 Sep 2021 22:33:54 +0200 Subject: [PATCH 17/25] View changelog of updatable mods Signed-off-by: DeathsGun --- .../modmanager/api/gui/list/ListWidget.kt | 7 +- .../modmanager/gui/ModUpdateInfoScreen.kt | 134 ++++++++++++++++++ .../modmanager/gui/ModsOverviewScreen.kt | 26 +++- .../gui/widget/DescriptionWidget.kt | 111 +++++++++++++++ .../xyz/deathsgun/modmanager/md/Markdown.kt | 68 +++++++++ .../xyz/deathsgun/modmanager/update/Update.kt | 2 +- .../modmanager/update/UpdateManager.kt | 6 +- .../assets/modmanager/lang/en_us.json | 8 +- 8 files changed, 349 insertions(+), 13 deletions(-) create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/md/Markdown.kt diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/ListWidget.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/ListWidget.kt index 1386b03..9a14a10 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/ListWidget.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/ListWidget.kt @@ -28,6 +28,7 @@ abstract class ListWidget>( protected val parent: IListScreen ) : AlwaysSelectedEntryListWidget(client, width, height, top, bottom, itemHeight) { + var renderOutline: Boolean = true protected var selectedId: String? = null private var scrolling = false @@ -68,7 +69,7 @@ abstract class ListWidget>( val entry: Entry = getEntry(index) val rowWidth = this.rowWidth var entryLeft: Int - if (isSelectedEntry(index)) { + if (isSelectedEntry(index) && renderOutline) { entryLeft = rowLeft - 2 val selectionRight = x + rowWidth + 2 RenderSystem.disableTexture() @@ -140,6 +141,10 @@ abstract class ListWidget>( return super.getEntryCount() } + fun getBottom(): Int { + return bottom + } + abstract class Entry>(open val list: ListWidget, val id: String) : AlwaysSelectedEntryListWidget.Entry() { @Suppress("UNCHECKED_CAST") diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt new file mode 100644 index 0000000..7aa7ae2 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt @@ -0,0 +1,134 @@ +package xyz.deathsgun.modmanager.gui + +import com.mojang.blaze3d.systems.RenderSystem +import com.terraformersmc.modmenu.util.DrawingUtil +import net.minecraft.client.gui.DrawableHelper +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screen.ScreenTexts +import net.minecraft.client.gui.widget.ButtonWidget +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.text.* +import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.api.gui.list.IListScreen +import xyz.deathsgun.modmanager.gui.widget.DescriptionWidget +import xyz.deathsgun.modmanager.update.Update + + +class ModUpdateInfoScreen(private val previousScreen: Screen, private val update: Update) : + Screen(TranslatableText("modmanager.screen.update")), IListScreen { + + private lateinit var descriptionWidget: DescriptionWidget + private lateinit var updateButtonWidget: ButtonWidget + + override fun init() { + descriptionWidget = addSelectableChild( + DescriptionWidget( + client!!, + width - 20, + height - 34, + 79, + height - 30, + textRenderer.fontHeight, + this, + update.version.changelog + ) + ) + descriptionWidget.init() + descriptionWidget.setLeftPos(10) + val buttonX = width / 8 + addDrawableChild(ButtonWidget(buttonX, height - 25, 150, 20, ScreenTexts.BACK) { + client?.setScreen(previousScreen) + }) + updateButtonWidget = addDrawableChild(ButtonWidget( + this.width - buttonX - 150, this.height - 25, 150, 20, TranslatableText("modmanager.button.update") + ) { + installUpdate() + }) + } + + private fun installUpdate() { + this.updateButtonWidget.active = false + this.updateButtonWidget.message = TranslatableText("modmanager.message.updating") + } + + override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { + renderBackground(matrices) + descriptionWidget.render(matrices, mouseX, mouseY, delta) + + val iconSize = 64 + ModManager.modManager.icons.bindIcon(update.mod) + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F) + ModManager.modManager.icons.bindIcon(update.mod) + RenderSystem.enableBlend() + DrawableHelper.drawTexture(matrices, 20, 10, 0.0F, 0.0F, iconSize, iconSize, iconSize, iconSize) + RenderSystem.disableBlend() + + val font = client!!.textRenderer + var trimmedTitle: MutableText = LiteralText(font.trimToWidth(update.mod.name, width - 200)) + trimmedTitle = trimmedTitle.setStyle(Style.EMPTY.withBold(true)) + + var detailsY = 15 + var textX = 20 + iconSize + 5 + + font.draw(matrices, trimmedTitle, textX.toFloat(), detailsY.toFloat(), 0xFFFFFF) + + if (update.mod.author != null) { + detailsY += 12 + font.draw( + matrices, + TranslatableText("modmanager.details.author", update.mod.author), + textX.toFloat(), + detailsY.toFloat(), + 0xFFFFFF + ) + } + + detailsY += 12 + font.draw( + matrices, + TranslatableText("modmanager.details.versioning", update.installedVersion, update.version.version), + textX.toFloat(), + detailsY.toFloat(), + 0xFFFFFF + ) + + if (update.mod.license != null) { + detailsY += 12 + DrawingUtil.drawBadge( + matrices, + textX, + detailsY, + font.getWidth(update.mod.license) + 6, + Text.of(update.mod.license).asOrderedText(), + -0x909396, + -0xcecfd1, + 0xCACACA + ) + } + + for (category in update.mod.categories) { + val textWidth: Int = font.getWidth(category.text) + 6 + DrawingUtil.drawBadge( + matrices, + textX, + detailsY + 14, + textWidth, + category.text.asOrderedText(), + -0x909396, + -0xcecfd1, + 0xCACACA + ) + textX += textWidth + 4 + } + + super.render(matrices, mouseX, mouseY, delta) + } + + override fun onClose() { + client?.setScreen(previousScreen) + } + + override fun updateSelectedEntry(widget: Any, entry: E?) { + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt index 6503b3b..1ff1af4 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt @@ -49,6 +49,7 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab private lateinit var categoryList: CategoryListWidget private lateinit var nextPage: ButtonWidget private lateinit var previousPage: ButtonWidget + private lateinit var updateAllButtons: ButtonWidget @OptIn(DelicateCoroutinesApi::class) override fun init() { @@ -103,6 +104,14 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab TranslatableText("modmanager.page.next") ) { showNextPage() }) + updateAllButtons = addDrawableChild(ButtonWidget( + width - 10 - 160, + 10, + 160, + 20, + TranslatableText("modmanager.button.updateAll") + ) { updateAll() }) + GlobalScope.launch { val provider = ModManager.modManager.getSelectedProvider() ?: return@launch when (val result = provider.getCategories()) { @@ -145,6 +154,10 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab //TODO: Paging } + private fun updateAll() { + this.updateAllButtons.message = TranslatableText("modmanager.message.updating") + } + private fun showNextPage() { this.page++ if (selectedCategory == null) { @@ -184,11 +197,9 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab selectedCategory ?: return val provider = ModManager.modManager.getSelectedProvider() ?: return if (selectedCategory!!.id == "updatable") { - GlobalScope.launch { - modList.clear() - ModManager.modManager.update.updates.forEach { - modList.add(it.mod) - } + modList.clear() + ModManager.modManager.update.updates.forEach { + modList.add(it.mod) } return } @@ -231,6 +242,10 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab return } if (selectedMod == entry) { + if (selectedCategory?.id == "updatable") { + val update = ModManager.modManager.update.getUpdateForMod(selectedMod!!.mod) ?: return + client?.setScreen(ModUpdateInfoScreen(this, update)) + } return // TODO: Open detail view } selectedMod = entry as ModListEntry @@ -252,6 +267,7 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab this.searchField.tick() this.previousPage.active = page > 0 this.nextPage.active = this.modList.getElementCount() >= limit + this.updateAllButtons.visible = selectedCategory?.id == "updatable" && this.modList.getElementCount() > 0 } override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.kt new file mode 100644 index 0000000..062d562 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.kt @@ -0,0 +1,111 @@ +package xyz.deathsgun.modmanager.gui.widget + +import net.minecraft.client.MinecraftClient +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.text.* +import net.minecraft.util.Util +import xyz.deathsgun.modmanager.api.gui.list.IListScreen +import xyz.deathsgun.modmanager.api.gui.list.ListWidget +import xyz.deathsgun.modmanager.md.Markdown + + +class DescriptionWidget( + client: MinecraftClient, + width: Int, + height: Int, + top: Int, + bottom: Int, + itemHeight: Int, + parent: IListScreen, + val text: String +) : ListWidget(client, width, height, top, bottom, itemHeight, parent) { + + private val textRenderer = MinecraftClient.getInstance().textRenderer + + override fun init() { + renderOutline = false + val lines = Markdown(text).toText() + for (line in lines) { + if (textRenderer.getWidth(line) >= width - 10) { + val texts: List = textRenderer.wrapLines(line, width - 10) + for (wrappedLine in texts) { + addEntry(Entry(this, getText(wrappedLine))) + } + continue + } + addEntry(Entry(this, line)) + } + addEntry(Entry(this, LiteralText(""))) + } + + override fun getSelectedOrNull(): Entry? { + return null + } + + override fun getRowWidth(): Int { + return width - 10 + } + + override fun getScrollbarPositionX(): Int { + return width - 6 + left + } + + private fun getText(orderedText: OrderedText): LiteralText { + val fields = orderedText.javaClass.declaredFields + var text = "" + var style = Style.EMPTY + for (field in fields) { + field.isAccessible = true + if (field.get(orderedText) is String) { + text = field.get(orderedText) as String + } + if (field.get(orderedText) is Style) { + style = field.get(orderedText) as Style + } + } + return LiteralText(text).apply { this.style = style } + } + + class Entry(list: ListWidget, val text: Text) : ListWidget.Entry(list, text.string) { + + private val textRenderer = MinecraftClient.getInstance().textRenderer + var x: Int = 0 + + override fun render( + matrices: MatrixStack?, + index: Int, + y: Int, + x: Int, + entryWidth: Int, + entryHeight: Int, + mouseX: Int, + mouseY: Int, + hovered: Boolean, + tickDelta: Float + ) { + if (y >= list.getBottom() - textRenderer.fontHeight + 2) { + return + } + textRenderer.draw(matrices, text, x.toFloat(), y.toFloat(), 0xFFFFFF) + } + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + if (isMouseOver(mouseX, mouseY)) { + val event = text.style.clickEvent + if (event == null || event.action != ClickEvent.Action.OPEN_URL) { + return super.mouseClicked(mouseX, mouseY, button) + } + Util.getOperatingSystem().open(event.value) + } + return super.mouseClicked(mouseX, mouseY, button) + } + + override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean { + return super.isMouseOver(mouseX, mouseY) && x + textRenderer.getWidth(text) >= mouseX + } + + override fun getNarration(): Text { + return text + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/md/Markdown.kt b/src/main/kotlin/xyz/deathsgun/modmanager/md/Markdown.kt new file mode 100644 index 0000000..88171fc --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/md/Markdown.kt @@ -0,0 +1,68 @@ +package xyz.deathsgun.modmanager.md + +import net.minecraft.text.ClickEvent +import net.minecraft.text.LiteralText +import net.minecraft.text.MutableText +import net.minecraft.util.Formatting +import java.util.regex.Matcher +import java.util.regex.Pattern + + +class Markdown(private var text: String) { + + private val boldPattern: Pattern = Pattern.compile("\\*\\*(.*?)\\*\\*") + private val linkPattern: Pattern = Pattern.compile("\\[(.*?)]\\((.*?)\\)") + private val imagePattern: Pattern = Pattern.compile("!\\[(.*?)]\\((.*?)\\)") + + fun toText(): List { + text = text.replace("\\u00A0", " ") + text = text.replace("\r".toRegex(), "") + text = text.replace("
".toRegex(), "\n").replace("
".toRegex(), "\n") + val lines = text.split("\n").toTypedArray() + val texts = ArrayList() + for (line in lines) { + if (imagePattern.matcher(line).find()) { + continue + } + texts.add(processLine(line)) + } + return texts + } + + private fun processLine(text: String): MutableText { + if (boldPattern.matcher(text).find()) { + return extractBoldText(text) + } + return if (linkPattern.matcher(text).find()) { + extractLinkText(text) + } else LiteralText(text) + } + + private fun extractLinkText(text: String): MutableText { + val matcher: Matcher = linkPattern.matcher(text) + if (!matcher.find()) { + return LiteralText(text) + } + val linkText: String = matcher.group(1) + val begin = text.indexOf(linkText) + val preText = LiteralText(text.substring(0, begin).replace("\\[".toRegex(), "")) + val matchedText = LiteralText(linkText).formatted(Formatting.UNDERLINE, Formatting.BLUE) + matchedText.style = matchedText.style.withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, matcher.group(2))) + return preText.append(matchedText) + .append(extractLinkText(text.substring(begin + 3 + linkText.length + matcher.group(2).length))) + } + + private fun extractBoldText(text: String): MutableText { + val matcher: Matcher = boldPattern.matcher(text) + if (!matcher.find()) { + return LiteralText(text) + } + val boldText: String = matcher.group(1) + val begin = text.indexOf(boldText) + val preText = LiteralText(text.substring(0, begin).replace("\\*\\*".toRegex(), "")) + val matchedText = LiteralText(boldText).formatted(Formatting.BOLD) + return preText.append(matchedText).append(extractBoldText(text.substring(begin + 2 + boldText.length))) + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/update/Update.kt b/src/main/kotlin/xyz/deathsgun/modmanager/update/Update.kt index d092f85..e2fa239 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/update/Update.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/update/Update.kt @@ -19,4 +19,4 @@ package xyz.deathsgun.modmanager.update import xyz.deathsgun.modmanager.api.mod.Mod import xyz.deathsgun.modmanager.api.mod.Version -data class Update(val mod: Mod, val fabricId: String, val version: Version) +data class Update(val mod: Mod, val fabricId: String, val installedVersion: String, val version: Version) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt index 4cc81e0..742c9be 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt @@ -93,7 +93,7 @@ class UpdateManager { ModManager.modManager.setModState(metadata.id, metadata.id, ModState.OUTDATED) when (val modResult = provider.getMod(metadata.id)) { is ModResult.Success -> { - this.updates.add(Update(modResult.mod, metadata.id, version)) + this.updates.add(Update(modResult.mod, metadata.id, metadata.version.friendlyString, version)) } } return @@ -131,7 +131,7 @@ class UpdateManager { ModManager.modManager.setModState(metadata.id, mod.id, ModState.OUTDATED) when (val modResult = provider.getMod(mod.id)) { is ModResult.Success -> { - this.updates.add(Update(modResult.mod, metadata.id, version)) + this.updates.add(Update(modResult.mod, metadata.id, metadata.version.friendlyString, version)) } } } @@ -171,7 +171,7 @@ class UpdateManager { ModManager.modManager.setModState(metadata.id, id, ModState.OUTDATED) when (val modResult = ModManager.modManager.provider[provider.getName()]?.getMod(id)) { is ModResult.Success -> { - this.updates.add(Update(modResult.mod, metadata.id, version)) + this.updates.add(Update(modResult.mod, metadata.id, metadata.version.friendlyString, version)) } } } diff --git a/src/main/resources/assets/modmanager/lang/en_us.json b/src/main/resources/assets/modmanager/lang/en_us.json index 8446043..7d5fdfc 100644 --- a/src/main/resources/assets/modmanager/lang/en_us.json +++ b/src/main/resources/assets/modmanager/lang/en_us.json @@ -1,6 +1,10 @@ { "modmanager.badge.installed": "Installed", "modmanager.badge.outdated": "Outdated", + "modmanager.button.install": "Install", + "modmanager.button.update": "Update", + "modmanager.button.updateAll": "Update all", + "modmanager.button.remove": "Remove", "modmanager.categories": "Categories", "modmanager.category.updatable": "Updatable Mods", "modmanager.category.technology": "Technology", @@ -16,12 +20,10 @@ "modmanager.category.equipment": "Equipment", "modmanager.category.misc": "Miscellaneous", "modmanager.details.author": "By %s", + "modmanager.details.versioning": "From %s to %s", "modmanager.message.goBackAndTryAgain": "Go Back & Try Again", - "modmanager.message.install": "Install", "modmanager.message.installing": "Installing...", - "modmanager.message.update": "Update", "modmanager.message.updating": "Updating...", - "modmanager.message.remove": "Remove", "modmanager.message.removing": "Removing...", "modmanager.search": "Search...", "modmanager.message.showError": "Show Error", From 7557f9b16c324199ec2c70e5cac050e0d10a0205 Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Mon, 6 Sep 2021 23:32:02 +0200 Subject: [PATCH 18/25] Allow updating mods over update info view Signed-off-by: DeathsGun --- .../modmanager/api/ModUpdateResult.kt | 8 ++ .../xyz/deathsgun/modmanager/api/mod/Asset.kt | 3 +- .../modmanager/gui/ModUpdateInfoScreen.kt | 21 ++++++ .../modmanager/gui/ModsOverviewScreen.kt | 1 + .../modmanager/providers/modrinth/Modrinth.kt | 2 +- .../modrinth/models/ModrinthVersion.kt | 3 +- .../modmanager/update/UpdateManager.kt | 73 +++++++++++++++++++ 7 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/api/ModUpdateResult.kt diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/ModUpdateResult.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/ModUpdateResult.kt new file mode 100644 index 0000000..86fc279 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/ModUpdateResult.kt @@ -0,0 +1,8 @@ +package xyz.deathsgun.modmanager.api + +import net.minecraft.text.TranslatableText + +sealed class ModUpdateResult { + object Success : ModUpdateResult() + data class Error(val text: TranslatableText, val cause: Exception? = null) : ModUpdateResult() +} diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Asset.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Asset.kt index 555a823..6cc2da6 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Asset.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Asset.kt @@ -19,5 +19,6 @@ package xyz.deathsgun.modmanager.api.mod data class Asset( val url: String, val filename: String, - val hashes: Map + val hashes: Map, + val primary: Boolean ) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt index 7aa7ae2..0fe2a66 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt @@ -2,13 +2,19 @@ package xyz.deathsgun.modmanager.gui import com.mojang.blaze3d.systems.RenderSystem import com.terraformersmc.modmenu.util.DrawingUtil +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.DrawableHelper import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.screen.ScreenTexts import net.minecraft.client.gui.widget.ButtonWidget import net.minecraft.client.util.math.MatrixStack import net.minecraft.text.* +import org.apache.logging.log4j.LogManager import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.api.ModUpdateResult import xyz.deathsgun.modmanager.api.gui.list.IListScreen import xyz.deathsgun.modmanager.gui.widget.DescriptionWidget import xyz.deathsgun.modmanager.update.Update @@ -46,9 +52,24 @@ class ModUpdateInfoScreen(private val previousScreen: Screen, private val update }) } + @OptIn(DelicateCoroutinesApi::class) private fun installUpdate() { this.updateButtonWidget.active = false this.updateButtonWidget.message = TranslatableText("modmanager.message.updating") + GlobalScope.launch { + when (val result = ModManager.modManager.update.updateMod(update)) { + is ModUpdateResult.Error -> { + this@ModUpdateInfoScreen.updateButtonWidget.active = true + this@ModUpdateInfoScreen.updateButtonWidget.message = TranslatableText("modmanager.button.update") + LogManager.getLogger().error(result.text.key, result.cause) + } + is ModUpdateResult.Success -> { + client!!.send { + MinecraftClient.getInstance().setScreen(previousScreen) + } + } + } + } } override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt index 1ff1af4..f2d53b2 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt @@ -154,6 +154,7 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab //TODO: Paging } + @OptIn(DelicateCoroutinesApi::class) private fun updateAll() { this.updateAllButtons.message = TranslatableText("modmanager.message.updating") } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt index ba8ea2e..f497d89 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt @@ -210,7 +210,7 @@ class Modrinth : IModProvider, IModUpdateProvider { for (modVersion in modrinthVersions) { val assets = ArrayList() for (file in modVersion.files) { - assets.add(Asset(file.url, file.filename, file.hashes)) + assets.add(Asset(file.url, file.filename, file.hashes, file.primary)) } versions.add( Version( diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModrinthVersion.kt b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModrinthVersion.kt index 09569c8..90a1ae9 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModrinthVersion.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/models/ModrinthVersion.kt @@ -35,6 +35,7 @@ data class ModrinthVersion( data class File( val hashes: Map, val filename: String, - val url: String + val url: String, + val primary: Boolean ) } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt index 742c9be..1e1f97e 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt @@ -26,8 +26,11 @@ import net.fabricmc.loader.api.FabricLoader import net.fabricmc.loader.api.SemanticVersion import net.fabricmc.loader.api.metadata.ModMetadata import net.fabricmc.loader.util.version.VersionDeserializer +import net.minecraft.text.TranslatableText +import net.minecraft.util.Util import org.apache.logging.log4j.LogManager import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.api.ModUpdateResult import xyz.deathsgun.modmanager.api.http.ModResult import xyz.deathsgun.modmanager.api.http.ModsResult import xyz.deathsgun.modmanager.api.http.VersionResult @@ -36,8 +39,15 @@ import xyz.deathsgun.modmanager.api.mod.Version import xyz.deathsgun.modmanager.api.provider.IModUpdateProvider import xyz.deathsgun.modmanager.models.FabricMetadata import xyz.deathsgun.modmanager.state.ModState +import java.math.BigInteger +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse import java.nio.file.Files import java.nio.file.Path +import java.security.MessageDigest +import java.util.concurrent.TimeUnit import java.util.stream.Collectors import java.util.zip.ZipFile import kotlin.io.path.name @@ -47,6 +57,7 @@ class UpdateManager { private val logger = LogManager.getLogger("UpdateCheck") private val blockedIds = arrayOf("java", "minecraft") + private val http: HttpClient = HttpClient.newHttpClient() val updates = ArrayList() suspend fun checkUpdates() = coroutineScope { @@ -204,6 +215,49 @@ class UpdateManager { return latest } + fun updateMod(update: Update): ModUpdateResult { + val oldUpdate = FabricLoader.getInstance().allMods.find { it.metadata.id == update.fabricId } + ?: return ModUpdateResult.Error(TranslatableText("modmanager.error.container.notFound")) + val oldJar = findJarByModContainer(oldUpdate.metadata) + ?: return ModUpdateResult.Error(TranslatableText("modmanager.error.jar.notFound")) + try { + oldJar.forceDelete() + } catch (e: Exception) { + return ModUpdateResult.Error(TranslatableText("modmanager.error.jar.failedDelete", e)) + } + val asset = + update.version.assets.find { (it.filename.endsWith(".jar") || it.primary) && !it.filename.contains("forge") } + ?: return ModUpdateResult.Error(TranslatableText("modmanager.error.update.noFabricJar")) + val newJar = oldJar.resolveSibling(asset.filename) // Download into same directory where the old jar was + + val request = HttpRequest.newBuilder(URI.create(asset.url)).GET() + .setHeader("User-Agent", "ModManager ${ModManager.getVersion()}").build() + return try { + val response = this.http.send(request, HttpResponse.BodyHandlers.ofFile(newJar)) + if (response.statusCode() != 200) { + ModUpdateResult.Error(TranslatableText("modmanager.error.invalidStatus", response.statusCode())) + } + val expected = asset.hashes["sha512"] + val calculated = newJar.sha512() + if (calculated != expected) { + return ModUpdateResult.Error( + TranslatableText( + "modmanager.error.invalidHash", + "SHA-512", + expected, + calculated + ) + ) + } + ModManager.modManager.setModState(update.fabricId, update.mod.id, ModState.INSTALLED) + this.updates.removeIf { it.fabricId == update.fabricId || it.mod.id == update.mod.id } + ModUpdateResult.Success + } catch (e: Exception) { + ModUpdateResult.Error(TranslatableText("modmanager.error.update.unknown", e)) + } + + } + private val json = Json { ignoreUnknownKeys = true } @@ -249,4 +303,23 @@ class UpdateManager { } } + private fun Path.forceDelete() { + if (Util.getOperatingSystem() == Util.OperatingSystem.WINDOWS) { + ProcessBuilder("del", "/f", this.name).directory(this.parent.toFile()).start() + .waitFor(200, TimeUnit.MILLISECONDS) + return + } + Files.deleteIfExists(this) + } + + private fun Path.sha512(): String { + val md: MessageDigest = MessageDigest.getInstance("SHA-512") + val messageDigest = md.digest(Files.readAllBytes(this)) + val no = BigInteger(1, messageDigest) + var hashtext: String = no.toString(16) + while (hashtext.length < 128) { + hashtext = "0$hashtext" + } + return hashtext + } } \ No newline at end of file From 962a780e197a55e19fecb52345901e49985e57e4 Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Wed, 8 Sep 2021 08:53:49 +0200 Subject: [PATCH 19/25] Cleanup icon cache if it reached over 10 MB Signed-off-by: DeathsGun --- .../xyz/deathsgun/modmanager/ModManager.kt | 1 + .../modmanager/gui/ModsOverviewScreen.kt | 1 - .../deathsgun/modmanager/icon/IconCache.kt | 32 +++++++++++++++++++ .../modmanager/update/UpdateManager.kt | 11 ++++--- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt index ebc01a5..4307ae2 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt @@ -68,6 +68,7 @@ class ModManager : ClientModInitializer { updateProvider[modrinth.getName().lowercase()] = modrinth GlobalScope.launch { update.checkUpdates() + icons.cleanupCache() } } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt index f2d53b2..c467bae 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt @@ -283,7 +283,6 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab } override fun onClose() { - super.onClose() ModManager.modManager.icons.destroyAll() client!!.setScreen(previousScreen) } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/icon/IconCache.kt b/src/main/kotlin/xyz/deathsgun/modmanager/icon/IconCache.kt index 31b13f5..988bea1 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/icon/IconCache.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/icon/IconCache.kt @@ -25,6 +25,7 @@ import net.minecraft.client.MinecraftClient import net.minecraft.client.texture.NativeImage import net.minecraft.client.texture.NativeImageBackedTexture import net.minecraft.util.Identifier +import org.apache.commons.io.FileUtils import org.apache.logging.log4j.LogManager import xyz.deathsgun.modmanager.ModManager import xyz.deathsgun.modmanager.api.mod.Mod @@ -33,6 +34,7 @@ import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse import java.nio.file.Files +import java.util.stream.Collectors class IconCache { @@ -105,6 +107,36 @@ class IconCache { } fun destroyAll() { + for ((mod, state) in state) { + if (state != IconState.LOADED) { + continue + } + val icon = Identifier("modmanager", "mod_icons/${mod.lowercase()}") + MinecraftClient.getInstance().textureManager.destroyTexture(icon) + this.state[mod] = IconState.DOWNLOADED + } + } + + fun cleanupCache() { + logger.info("Starting cleanup...") + var files = Files.list(iconsDir) + .sorted { o1, o2 -> + o1.toFile().lastModified().compareTo(o2.toFile().lastModified()) + }.collect(Collectors.toList()) + if (files.isEmpty()) { + return + } + while (FileUtils.sizeOfDirectory(iconsDir.toFile()) >= 10000000) { + if (files.isEmpty()) { + return + } + Files.delete(files[0]) + files = Files.list(iconsDir) + .sorted { o1, o2 -> + o1.toFile().lastModified().compareTo(o2.toFile().lastModified()) + }.collect(Collectors.toList()) + } + logger.info("Cleanup done!") } private enum class IconState { diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt index 1e1f97e..db98158 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt @@ -305,7 +305,8 @@ class UpdateManager { private fun Path.forceDelete() { if (Util.getOperatingSystem() == Util.OperatingSystem.WINDOWS) { - ProcessBuilder("del", "/f", this.name).directory(this.parent.toFile()).start() + // Under windows this file gets locked can't be deleted by Java + ProcessBuilder("cmd", "/c", "del /f ${this.name}").directory(this.parent.toFile()).start() .waitFor(200, TimeUnit.MILLISECONDS) return } @@ -316,10 +317,10 @@ class UpdateManager { val md: MessageDigest = MessageDigest.getInstance("SHA-512") val messageDigest = md.digest(Files.readAllBytes(this)) val no = BigInteger(1, messageDigest) - var hashtext: String = no.toString(16) - while (hashtext.length < 128) { - hashtext = "0$hashtext" + var hashText: String = no.toString(16) + while (hashText.length < 128) { + hashText = "0$hashText" } - return hashtext + return hashText } } \ No newline at end of file From 6819083e1933fc83abee77896a969c7776601857 Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Wed, 8 Sep 2021 12:24:47 +0200 Subject: [PATCH 20/25] Added restart notice Also added docs more docs for the providers Closes #30 Signed-off-by: DeathsGun --- .../xyz/deathsgun/modmanager/ModManager.kt | 1 + .../modmanager/api/provider/IModProvider.kt | 10 +--------- .../api/provider/IModUpdateProvider.kt | 15 +++++++++++++-- .../modmanager/gui/ModsOverviewScreen.kt | 19 ++++++++++++++++++- .../modmanager/update/UpdateManager.kt | 1 + .../assets/modmanager/lang/en_us.json | 2 ++ 6 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt index 4307ae2..66ddf1c 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/ModManager.kt @@ -35,6 +35,7 @@ class ModManager : ClientModInitializer { private val states = ArrayList() lateinit var config: Config + var changed: Boolean = false val update: UpdateManager = UpdateManager() val icons: IconCache = IconCache() val provider: HashMap = HashMap() diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt index 9910158..97f4b74 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt @@ -22,15 +22,7 @@ import xyz.deathsgun.modmanager.api.http.ModsResult import xyz.deathsgun.modmanager.api.mod.Category import xyz.deathsgun.modmanager.api.mod.Mod -interface IModProvider { - - /** - * Name of the provider. This will be shown - * in the GUI - * - * @return returns a user-friendly name of the mod provider implementation - */ - fun getName(): String +interface IModProvider : IModUpdateProvider { /** * Returns a list of all possible mod categories also with an translatable text diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModUpdateProvider.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModUpdateProvider.kt index 955bfda..2e4837d 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModUpdateProvider.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModUpdateProvider.kt @@ -20,8 +20,19 @@ import xyz.deathsgun.modmanager.api.http.VersionResult interface IModUpdateProvider { - fun getVersionsForMod(id: String): VersionResult - + /** + * Name of the provider. This will be shown + * in the GUI + * + * @return returns a user-friendly name of the mod provider implementation + */ fun getName(): String + /** + * Gets a list all versions that can be downloaded for the specified [id] + * @return a returns a [VersionResult] which can be an [VersionResult.Error] or [VersionResult.Success] + * which contains a list of [xyz.deathsgun.modmanager.api.mod.Version] + */ + fun getVersionsForMod(id: String): VersionResult + } \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt index c467bae..0bfa755 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import net.minecraft.client.gui.Element +import net.minecraft.client.gui.screen.ConfirmScreen import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.widget.ButtonWidget import net.minecraft.client.gui.widget.TextFieldWidget @@ -284,6 +285,22 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab override fun onClose() { ModManager.modManager.icons.destroyAll() - client!!.setScreen(previousScreen) + if (!ModManager.modManager.changed) { + client!!.setScreen(previousScreen) + return + } + client!!.setScreen( + ConfirmScreen( + { + if (it) { + client!!.scheduleStop() + return@ConfirmScreen + } + client!!.setScreen(previousScreen) + }, + TranslatableText("modmanager.changes.title"), + TranslatableText("modmanager.changes.message") + ) + ) } } \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt index db98158..7c89e40 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt @@ -251,6 +251,7 @@ class UpdateManager { } ModManager.modManager.setModState(update.fabricId, update.mod.id, ModState.INSTALLED) this.updates.removeIf { it.fabricId == update.fabricId || it.mod.id == update.mod.id } + ModManager.modManager.changed = true ModUpdateResult.Success } catch (e: Exception) { ModUpdateResult.Error(TranslatableText("modmanager.error.update.unknown", e)) diff --git a/src/main/resources/assets/modmanager/lang/en_us.json b/src/main/resources/assets/modmanager/lang/en_us.json index 7d5fdfc..9fb7fc8 100644 --- a/src/main/resources/assets/modmanager/lang/en_us.json +++ b/src/main/resources/assets/modmanager/lang/en_us.json @@ -19,6 +19,8 @@ "modmanager.category.food": "Food", "modmanager.category.equipment": "Equipment", "modmanager.category.misc": "Miscellaneous", + "modmanager.changes.title": "§e§lChanges detected§r", + "modmanager.changes.message": "Changes will take effect after a relaunch.\nQuit Minecraft now?", "modmanager.details.author": "By %s", "modmanager.details.versioning": "From %s to %s", "modmanager.message.goBackAndTryAgain": "Go Back & Try Again", From 1b8a24a262bea7b4e3350c1381285074656054ca Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Wed, 8 Sep 2021 14:17:55 +0200 Subject: [PATCH 21/25] Error handling and update all Signed-off-by: DeathsGun --- .../modmanager/api/gui/list/ListWidget.kt | 30 +++++++-- .../modmanager/gui/ModUpdateInfoScreen.kt | 18 +++++- .../modmanager/gui/ModsOverviewScreen.kt | 61 +++++++++++++------ .../gui/widget/DescriptionWidget.kt | 18 +++++- .../modmanager/gui/widget/ErrorWidget.kt | 28 --------- .../modmanager/providers/modrinth/Modrinth.kt | 25 ++++---- .../modmanager/update/UpdateManager.kt | 3 +- .../assets/modmanager/lang/en_us.json | 14 ++--- .../assets/modmanager/lang/ko_kr.json | 10 +-- .../assets/modmanager/lang/ru_ru.json | 10 +-- .../assets/modmanager/lang/tr_tr.json | 10 +-- .../assets/modmanager/lang/zh_cn.json | 10 +-- 12 files changed, 127 insertions(+), 110 deletions(-) delete mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ErrorWidget.kt diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/ListWidget.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/ListWidget.kt index 9a14a10..12919d5 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/ListWidget.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/ListWidget.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package xyz.deathsgun.modmanager.api.gui.list import com.mojang.blaze3d.systems.RenderSystem @@ -23,13 +39,13 @@ abstract class ListWidget>( width: Int, height: Int, top: Int, - bottom: Int, + var bottom: Int, itemHeight: Int, - protected val parent: IListScreen + private val parent: IListScreen ) : AlwaysSelectedEntryListWidget(client, width, height, top, bottom, itemHeight) { var renderOutline: Boolean = true - protected var selectedId: String? = null + private var selectedId: String? = null private var scrolling = false abstract fun init() @@ -141,8 +157,12 @@ abstract class ListWidget>( return super.getEntryCount() } - fun getBottom(): Int { - return bottom + public override fun remove(index: Int): E? { + return super.remove(index) + } + + public override fun getEntry(index: Int): E { + return super.getEntry(index) } abstract class Entry>(open val list: ListWidget, val id: String) : diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt index 0fe2a66..52df163 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package xyz.deathsgun.modmanager.gui import com.mojang.blaze3d.systems.RenderSystem @@ -55,7 +71,7 @@ class ModUpdateInfoScreen(private val previousScreen: Screen, private val update @OptIn(DelicateCoroutinesApi::class) private fun installUpdate() { this.updateButtonWidget.active = false - this.updateButtonWidget.message = TranslatableText("modmanager.message.updating") + this.updateButtonWidget.message = TranslatableText("modmanager.status.updating") GlobalScope.launch { when (val result = ModManager.modManager.update.updateMod(update)) { is ModUpdateResult.Error -> { diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt index 0bfa755..44039f9 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt @@ -28,12 +28,16 @@ import net.minecraft.client.util.math.MatrixStack import net.minecraft.text.TranslatableText import org.lwjgl.glfw.GLFW import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.api.ModUpdateResult import xyz.deathsgun.modmanager.api.gui.list.IListScreen import xyz.deathsgun.modmanager.api.http.CategoriesResult import xyz.deathsgun.modmanager.api.http.ModsResult import xyz.deathsgun.modmanager.api.mod.Category import xyz.deathsgun.modmanager.api.provider.Sorting -import xyz.deathsgun.modmanager.gui.widget.* +import xyz.deathsgun.modmanager.gui.widget.CategoryListEntry +import xyz.deathsgun.modmanager.gui.widget.CategoryListWidget +import xyz.deathsgun.modmanager.gui.widget.ModListEntry +import xyz.deathsgun.modmanager.gui.widget.ModListWidget class ModsOverviewScreen(private val previousScreen: Screen) : Screen(TranslatableText("modmanager.title.overview")), IListScreen { @@ -45,7 +49,7 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab private var limit: Int = 20 private var scrollPercentage: Double = 0.0 private lateinit var searchField: TextFieldWidget - private lateinit var errorWidget: ErrorWidget + private var error: TranslatableText? = null private lateinit var modList: ModListWidget private lateinit var categoryList: CategoryListWidget private lateinit var nextPage: ButtonWidget @@ -80,8 +84,6 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab ) categoryList.setLeftPos(10) - errorWidget = ErrorWidget(client!!, 115, 35, width - 20, height - 150) - modList = addSelectableChild(ModListWidget(client!!, width - 10 - 115, height, 35, height - 30, 36, this)) modList.setLeftPos(115) @@ -117,10 +119,11 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab val provider = ModManager.modManager.getSelectedProvider() ?: return@launch when (val result = provider.getCategories()) { is CategoriesResult.Error -> { - errorWidget.error = result.text + error = result.text + return@launch } is CategoriesResult.Success -> { - errorWidget.error = null + error = null categoryList.clear() if (ModManager.modManager.update.updates.isNotEmpty()) { categoryList.add(Category("updatable", TranslatableText("modmanager.category.updatable"))) @@ -137,10 +140,11 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab if (query.isNotEmpty()) { when (val result = provider.search(query, page, limit)) { is ModsResult.Error -> { - errorWidget.error = result.text + error = result.text + return@launch } is ModsResult.Success -> { - errorWidget.error = null + error = null modList.setMods(result.mods) modList.scrollAmount = scrollPercentage } @@ -157,7 +161,23 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab @OptIn(DelicateCoroutinesApi::class) private fun updateAll() { - this.updateAllButtons.message = TranslatableText("modmanager.message.updating") + this.updateAllButtons.active = false + this.updateAllButtons.message = TranslatableText("modmanager.status.updating") + GlobalScope.launch { + val updates = ModManager.modManager.update.updates + for (update in ArrayList(updates)) { + when (val result = ModManager.modManager.update.updateMod(update)) { + is ModUpdateResult.Error -> error = result.text + } + } + if (error == null) { + updateAllButtons.active = true + updateAllButtons.message = TranslatableText("modmanager.button.update") + modList.clear() + updateAllButtons.visible = false + return@launch + } + } } private fun showNextPage() { @@ -185,10 +205,10 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab val provider = ModManager.modManager.getSelectedProvider() ?: return when (val result = provider.getMods(Sorting.RELEVANCE, page, limit)) { is ModsResult.Error -> { - errorWidget.error = result.text + error = result.text } is ModsResult.Success -> { - errorWidget.error = null + error = null modList.setMods(result.mods) } } @@ -207,10 +227,10 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab } when (val result = provider.getMods(selectedCategory!!.category, page, limit)) { is ModsResult.Error -> { - errorWidget.error = result.text + error = result.text } is ModsResult.Success -> { - errorWidget.error = null + error = null modList.setMods(result.mods) } } @@ -221,11 +241,11 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab val provider = ModManager.modManager.getSelectedProvider() ?: return true return when (val result = provider.search(this.query, 0, 20)) { is ModsResult.Error -> { - this.errorWidget.error = result.text + this.error = result.text true } is ModsResult.Success -> { - this.errorWidget.error = null + this.error = null this.modList.setMods(result.mods) true } @@ -274,11 +294,16 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { this.renderBackground(matrices) - this.errorWidget.render(matrices, mouseX, mouseY, delta) - if (this.errorWidget.error == null) { + if (this.error != null) { + textRenderer.draw(matrices, TranslatableText("modmanager.error.title"), 10F, 35F, 0xFFFFFF) + val lines = textRenderer.wrapLines(error!!, width - 20) + for (line in lines) { + textRenderer.draw(matrices, line, 10F, 47F, 0xFFFFFF) + } + } else { + this.categoryList.render(matrices, mouseX, mouseY, delta) this.modList.render(matrices, mouseX, mouseY, delta) } - this.categoryList.render(matrices, mouseX, mouseY, delta) this.searchField.render(matrices, mouseX, mouseY, delta) super.render(matrices, mouseX, mouseY, delta) } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.kt index 062d562..bf073c4 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package xyz.deathsgun.modmanager.gui.widget import net.minecraft.client.MinecraftClient @@ -83,7 +99,7 @@ class DescriptionWidget( hovered: Boolean, tickDelta: Float ) { - if (y >= list.getBottom() - textRenderer.fontHeight + 2) { + if (y >= list.bottom - textRenderer.fontHeight + 2) { return } textRenderer.draw(matrices, text, x.toFloat(), y.toFloat(), 0xFFFFFF) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ErrorWidget.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ErrorWidget.kt deleted file mode 100644 index 0424c70..0000000 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/ErrorWidget.kt +++ /dev/null @@ -1,28 +0,0 @@ -package xyz.deathsgun.modmanager.gui.widget - -import com.terraformersmc.modmenu.util.DrawingUtil -import net.minecraft.client.MinecraftClient -import net.minecraft.client.gui.DrawableHelper -import net.minecraft.client.util.math.MatrixStack -import net.minecraft.text.TranslatableText - -class ErrorWidget( - private val client: MinecraftClient, - private val x: Int, - private val y: Int, - private val width: Int, - private val height: Int -) : - DrawableHelper() { - - var error: TranslatableText? = null - - fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { - if (error == null) { - return - } - val lines: Int = height / client.textRenderer.fontHeight - DrawingUtil.drawWrappedString(matrices, error!!.asString(), x, y, width, lines, 0xFFFFFF) - } - -} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt index f497d89..3131f2c 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt @@ -38,6 +38,7 @@ import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse +import java.time.Duration @OptIn(ExperimentalSerializationApi::class) class Modrinth : IModProvider, IModUpdateProvider { @@ -46,7 +47,7 @@ class Modrinth : IModProvider, IModUpdateProvider { private val categories: ArrayList = ArrayList() private val cache: HashMap = HashMap() private val baseUri = "https://api.modrinth.com" - private val http = HttpClient.newHttpClient() + private val http = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build() override fun getName(): String { @@ -59,8 +60,8 @@ class Modrinth : IModProvider, IModUpdateProvider { } val request = HttpRequest.newBuilder().GET().setHeader("User-Agent", "ModManager " + ModManager.getVersion()) .uri(URI.create("${baseUri}/api/v1/tag/category")).build() - val response = this.http.send(request, HttpResponse.BodyHandlers.ofString()) return try { + val response = this.http.send(request, HttpResponse.BodyHandlers.ofString()) val categories = json.decodeFromString>(response.body()) for (category in categories) { this.categories.add( @@ -73,7 +74,7 @@ class Modrinth : IModProvider, IModUpdateProvider { CategoriesResult.Success(this.categories) } catch (e: Exception) { logger.error("Error while getting categories: ", e.message) - CategoriesResult.Error(TranslatableText("modmanager.response.failedToParse"), e) + CategoriesResult.Error(TranslatableText("modmanager.error.failedToParse", e.message), e) } } @@ -107,7 +108,7 @@ class Modrinth : IModProvider, IModUpdateProvider { } result } catch (e: Exception) { - ModsResult.Error(TranslatableText("modmanager.error.unknown"), e) + ModsResult.Error(TranslatableText("modmanager.error.unknown", e), e) } } @@ -118,7 +119,7 @@ class Modrinth : IModProvider, IModUpdateProvider { return try { getMods(builder, page, limit) } catch (e: Exception) { - ModsResult.Error(TranslatableText("modmanager.error.unknown"), e) + ModsResult.Error(TranslatableText("modmanager.error.unknown", e.message), e) } } @@ -133,14 +134,14 @@ class Modrinth : IModProvider, IModUpdateProvider { .uri(builder.build()).build() val response = this.http.send(request, HttpResponse.BodyHandlers.ofString()) if (response.statusCode() != 200) { - return ModsResult.Error(TranslatableText("modmanager.error.invalidStatus")) + return ModsResult.Error(TranslatableText("modmanager.error.invalidStatus", response.statusCode())) } return try { val result = json.decodeFromString(response.body()) ModsResult.Success(result.toList()) } catch (e: Exception) { logger.error("Error while requesting mods", e.message) - ModsResult.Error(TranslatableText("modmanager.error.failedToParse")) + ModsResult.Error(TranslatableText("modmanager.error.failedToParse", e.message)) } } @@ -157,10 +158,10 @@ class Modrinth : IModProvider, IModUpdateProvider { this.http.send(request, HttpResponse.BodyHandlers.ofString()) } catch (e: Exception) { logger.error("Error while getting mod", e.message) - return ModResult.Error(TranslatableText("modmanager.error.network"), e) + return ModResult.Error(TranslatableText("modmanager.error.network", e.message), e) } if (response.statusCode() != 200) { - return ModResult.Error(TranslatableText("modmanager.error.invalidStatus")) + return ModResult.Error(TranslatableText("modmanager.error.invalidStatus", response.statusCode())) } return try { val result = json.decodeFromString(response.body()) @@ -187,7 +188,7 @@ class Modrinth : IModProvider, IModUpdateProvider { ) ) } catch (e: Exception) { - ModResult.Error(TranslatableText("modmanager.error.failedToParse"), e) + ModResult.Error(TranslatableText("modmanager.error.failedToParse", e.message), e) } } @@ -202,7 +203,7 @@ class Modrinth : IModProvider, IModUpdateProvider { return VersionResult.Error(TranslatableText("modmanager.error.network"), e) } if (response.statusCode() != 200) { - return VersionResult.Error(TranslatableText("modmanager.error.invalidStatus")) + return VersionResult.Error(TranslatableText("modmanager.error.invalidStatus", response.statusCode())) } return try { val modrinthVersions = json.decodeFromString>(response.body()) @@ -224,7 +225,7 @@ class Modrinth : IModProvider, IModUpdateProvider { } VersionResult.Success(versions) } catch (e: Exception) { - VersionResult.Error(TranslatableText("modmanager.error.failedToParse"), e) + VersionResult.Error(TranslatableText("modmanager.error.failedToParse", e.message), e) } } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt index 7c89e40..fec578c 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt @@ -47,6 +47,7 @@ import java.net.http.HttpResponse import java.nio.file.Files import java.nio.file.Path import java.security.MessageDigest +import java.time.Duration import java.util.concurrent.TimeUnit import java.util.stream.Collectors import java.util.zip.ZipFile @@ -57,7 +58,7 @@ class UpdateManager { private val logger = LogManager.getLogger("UpdateCheck") private val blockedIds = arrayOf("java", "minecraft") - private val http: HttpClient = HttpClient.newHttpClient() + private val http: HttpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(15)).build() val updates = ArrayList() suspend fun checkUpdates() = coroutineScope { diff --git a/src/main/resources/assets/modmanager/lang/en_us.json b/src/main/resources/assets/modmanager/lang/en_us.json index 9fb7fc8..6059736 100644 --- a/src/main/resources/assets/modmanager/lang/en_us.json +++ b/src/main/resources/assets/modmanager/lang/en_us.json @@ -1,10 +1,8 @@ { "modmanager.badge.installed": "Installed", "modmanager.badge.outdated": "Outdated", - "modmanager.button.install": "Install", "modmanager.button.update": "Update", "modmanager.button.updateAll": "Update all", - "modmanager.button.remove": "Remove", "modmanager.categories": "Categories", "modmanager.category.updatable": "Updatable Mods", "modmanager.category.technology": "Technology", @@ -23,15 +21,15 @@ "modmanager.changes.message": "Changes will take effect after a relaunch.\nQuit Minecraft now?", "modmanager.details.author": "By %s", "modmanager.details.versioning": "From %s to %s", - "modmanager.message.goBackAndTryAgain": "Go Back & Try Again", - "modmanager.message.installing": "Installing...", - "modmanager.message.updating": "Updating...", - "modmanager.message.removing": "Removing...", + "modmanager.error.title": "§4Error:§r", + "modmanager.error.unknown": "Unknown error while retrieving data", + "modmanager.error.invalidStatus": "Received invalid status code {}", + "modmanager.error.network": "Network error while retrieving data", + "modmanager.error.failedToParse": "Failed to parse response", + "modmanager.status.updating": "Updating...", "modmanager.search": "Search...", - "modmanager.message.showError": "Show Error", "modmanager.page.next": "Next Page", "modmanager.page.previous": "Previous Page", - "modmanager.title": "Browse Mods", "modmanager.toast.update.title": "Updates Available!", "modmanager.toast.update.description": "%d mods can be updated" } \ No newline at end of file diff --git a/src/main/resources/assets/modmanager/lang/ko_kr.json b/src/main/resources/assets/modmanager/lang/ko_kr.json index 55e5acc..4be3bf0 100644 --- a/src/main/resources/assets/modmanager/lang/ko_kr.json +++ b/src/main/resources/assets/modmanager/lang/ko_kr.json @@ -16,18 +16,10 @@ "modmanager.category.equipment": "도구", "modmanager.category.misc": "기타", "modmanager.details.author": "제작가: %s", - "modmanager.message.goBackAndTryAgain": "다시 시도하시오", - "modmanager.message.install": "설치", - "modmanager.message.installing": "설치중...", - "modmanager.message.update": "업데이트", - "modmanager.message.updating": "업데이트중...", - "modmanager.message.remove": "삭제", - "modmanager.message.removing": "삭제중...", + "modmanager.status.updating": "업데이트중...", "modmanager.search": "검색...", - "modmanager.message.showError": "에러 보이기", "modmanager.page.next": "다음", "modmanager.page.previous": "이전", - "modmanager.title": "모드 오버뷰", "modmanager.toast.update.title": "업데이트 가능!", "modmanager.toast.update.description": "%d 개의 모드가 업데이트 가능 합니다." } \ No newline at end of file diff --git a/src/main/resources/assets/modmanager/lang/ru_ru.json b/src/main/resources/assets/modmanager/lang/ru_ru.json index 6338450..c756324 100644 --- a/src/main/resources/assets/modmanager/lang/ru_ru.json +++ b/src/main/resources/assets/modmanager/lang/ru_ru.json @@ -16,18 +16,10 @@ "modmanager.category.equipment": "Экипировка", "modmanager.category.misc": "Разное", "modmanager.details.author": "Автор: %s", - "modmanager.message.goBackAndTryAgain": "Вернуться и повторить", - "modmanager.message.install": "Установить", - "modmanager.message.installing": "Установка...", - "modmanager.message.update": "Обновить", - "modmanager.message.updating": "Обновление...", - "modmanager.message.remove": "Удалить", - "modmanager.message.removing": "Удаление...", + "modmanager.status.updating": "Обновление...", "modmanager.search": "Поиск...", - "modmanager.message.showError": "Просмотр ошибки", "modmanager.page.next": "Следующая страница", "modmanager.page.previous": "Предыдущая страница", - "modmanager.title": "Обзор мода", "modmanager.toast.update.title": "Доступны обновления!", "modmanager.toast.update.description": "Можно обновить модов: %d" } diff --git a/src/main/resources/assets/modmanager/lang/tr_tr.json b/src/main/resources/assets/modmanager/lang/tr_tr.json index 1b668ff..f31e37f 100644 --- a/src/main/resources/assets/modmanager/lang/tr_tr.json +++ b/src/main/resources/assets/modmanager/lang/tr_tr.json @@ -16,18 +16,10 @@ "modmanager.category.equipment": "Teçhizat", "modmanager.category.misc": "Çeşitli", "modmanager.details.author": "%s tarafından", - "modmanager.message.goBackAndTryAgain": "Geri Dön ve Tekrar Dene", - "modmanager.message.install": "Kur", - "modmanager.message.installing": "Kuruluyor...", - "modmanager.message.update": "Güncelle", - "modmanager.message.updating": "Güncelleniyor...", - "modmanager.message.remove": "Kaldır", - "modmanager.message.removing": "Kaldırılıyor...", + "modmanager.status.updating": "Güncelleniyor...", "modmanager.search": "Mod Ara...", - "modmanager.message.showError": "Hatayı Göster", "modmanager.page.next": "Sıradaki Sayfa", "modmanager.page.previous": "Önceki Sayfa", - "modmanager.title": "Modları Keşfet", "modmanager.toast.update.title": "Güncelleme var!", "modmanager.toast.update.description": "%d tane mod için güncelleme mevcut." } diff --git a/src/main/resources/assets/modmanager/lang/zh_cn.json b/src/main/resources/assets/modmanager/lang/zh_cn.json index 65915c4..7b58550 100644 --- a/src/main/resources/assets/modmanager/lang/zh_cn.json +++ b/src/main/resources/assets/modmanager/lang/zh_cn.json @@ -16,18 +16,10 @@ "modmanager.category.equipment": "装备", "modmanager.category.misc": "杂项", "modmanager.details.author": "作者:%s", - "modmanager.message.goBackAndTryAgain": "返回并重试", - "modmanager.message.install": "安装", - "modmanager.message.installing": "安装中...", - "modmanager.message.update": "更新", - "modmanager.message.updating": "更新中...", - "modmanager.message.remove": "卸载", - "modmanager.message.removing": "卸载中...", + "modmanager.status.updating": "更新中...", "modmanager.search": "搜索...", - "modmanager.message.showError": "查看错误信息", "modmanager.page.next": "下一页", "modmanager.page.previous": "上一页", - "modmanager.title": "浏览模组", "modmanager.toast.update.title": "模组可更新!", "modmanager.toast.update.description": "%d个模组有更新可用" } \ No newline at end of file From 8d8ea91b5222a76c09396c2ed7a7125e68e2ff1e Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Sat, 11 Sep 2021 19:09:34 +0200 Subject: [PATCH 22/25] Improved error screen Signed-off-by: DeathsGun --- .../xyz/deathsgun/modmanager/api/mod/Mod.kt | 6 +- .../deathsgun/modmanager/gui/ErrorScreen.kt | 76 ++++++++++ .../modmanager/gui/ModDetailScreen.kt | 137 ++++++++++++++++++ .../modmanager/gui/ModsOverviewScreen.kt | 20 +-- .../gui/widget/DescriptionWidget.kt | 2 +- .../xyz/deathsgun/modmanager/md/Markdown.kt | 19 ++- .../modmanager/providers/modrinth/Modrinth.kt | 11 +- .../assets/modmanager/lang/en_us.json | 3 +- 8 files changed, 252 insertions(+), 22 deletions(-) create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/gui/ErrorScreen.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/gui/ModDetailScreen.kt diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt index c6f0fa2..9dd0fac 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/mod/Mod.kt @@ -19,11 +19,11 @@ package xyz.deathsgun.modmanager.api.mod data class Mod( val id: String, val slug: String, - val author: String?, + var author: String?, val name: String, - val shortDescription: String, + var shortDescription: String, val iconUrl: String?, - val description: String?, + var description: String?, val license: String?, val categories: List, ) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ErrorScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ErrorScreen.kt new file mode 100644 index 0000000..002709c --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ErrorScreen.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.gui + +import net.minecraft.client.font.MultilineText +import net.minecraft.client.gui.DrawableHelper +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screen.ScreenTexts +import net.minecraft.client.gui.widget.ButtonWidget +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.text.TranslatableText +import net.minecraft.util.math.MathHelper + +class ErrorScreen( + private val previousScreen: Screen, + private val actionScreen: Screen, + private val error: TranslatableText +) : + Screen(TranslatableText("modmanager.error.title")) { + + private lateinit var text: MultilineText + + override fun init() { + this.text = MultilineText.create(this.textRenderer, this.error, this.width - 50) + val linesHeight = this.text.count() * 9 + val bottom = MathHelper.clamp(90 + linesHeight + 12, this.height / 6 + 69, this.height - 24) + addDrawableChild( + ButtonWidget( + this.width / 2 - 155, + bottom, + 150, + 20, + ScreenTexts.BACK + ) { + client!!.setScreen(previousScreen) + } + ) + addDrawableChild( + ButtonWidget( + this.width / 2 + 5, + bottom, + 150, + 20, + TranslatableText("modmanager.button.tryAgain") + ) { + client!!.setScreen(actionScreen) + } + ) + } + + override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { + super.renderBackground(matrices) + DrawableHelper.drawCenteredText(matrices, this.textRenderer, this.title, this.width / 2, 70, 16777215) + this.text.drawCenterWithShadow(matrices, this.width / 2, 90) + super.render(matrices, mouseX, mouseY, delta) + } + + override fun shouldCloseOnEsc(): Boolean { + return false + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModDetailScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModDetailScreen.kt new file mode 100644 index 0000000..791b427 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModDetailScreen.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.gui + +import com.mojang.blaze3d.systems.RenderSystem +import com.terraformersmc.modmenu.util.DrawingUtil +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screen.ScreenTexts +import net.minecraft.client.gui.widget.ButtonWidget +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.text.* +import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.api.gui.list.IListScreen +import xyz.deathsgun.modmanager.api.http.ModResult +import xyz.deathsgun.modmanager.api.mod.Mod +import xyz.deathsgun.modmanager.gui.widget.DescriptionWidget + + +class ModDetailScreen(private val previousScreen: Screen, var mod: Mod) : Screen(LiteralText(mod.name)), IListScreen { + + private lateinit var descriptionWidget: DescriptionWidget + + init { + val provider = ModManager.modManager.getSelectedProvider() + if (provider != null) { + when (val result = provider.getMod(mod.id)) { + is ModResult.Error -> { + client!!.setScreen(ErrorScreen(previousScreen, this, result.text)) + } + is ModResult.Success -> { + val tmp = mod + mod = result.mod + mod.author = tmp.author + mod.shortDescription = tmp.shortDescription + } + } + } + } + + override fun init() { + val buttonX = width / 8 + descriptionWidget = addSelectableChild( + DescriptionWidget( + client!!, + width - 20, + height - 34, + 79, + height - 30, + textRenderer.fontHeight, + this, + mod.description!! + ) + ) + descriptionWidget.setLeftPos(10) + descriptionWidget.init() + addDrawableChild(ButtonWidget(buttonX, height - 28, 150, 20, ScreenTexts.BACK) { + client!!.setScreen(previousScreen) + }) + + } + + override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { + super.renderBackground(matrices) + descriptionWidget.render(matrices, mouseX, mouseY, delta) + + val iconSize = 64 + ModManager.modManager.icons.bindIcon(mod) + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f) + ModManager.modManager.icons.bindIcon(mod) + RenderSystem.enableBlend() + drawTexture(matrices, 20, 10, 0.0f, 0.0f, iconSize, iconSize, iconSize, iconSize) + RenderSystem.disableBlend() + + val font = client!!.textRenderer + + var trimmedTitle: MutableText = LiteralText(font.trimToWidth(mod.name, width - 200)) + trimmedTitle = trimmedTitle.setStyle(Style.EMPTY.withBold(true)) + + var detailsY = 15F + var textX = 20 + iconSize + 5F + + font.draw(matrices, trimmedTitle, textX, detailsY, 0xFFFFFF) + + detailsY += 12 + font.draw( + matrices, + TranslatableText("modmanager.details.author", mod.author), + textX, detailsY, 0xFFFFFF + ) + + detailsY += 12 + DrawingUtil.drawBadge( + matrices, textX.toInt(), detailsY.toInt(), + font.getWidth(mod.license) + 6, + Text.of(mod.license).asOrderedText(), + -0x909396, -0xcecfd1, 0xCACACA + ) + + for (category in mod.categories) { + val textWidth: Int = font.getWidth(category.text) + 6 + DrawingUtil.drawBadge( + matrices, + textX.toInt(), + (detailsY + 14).toInt(), + textWidth, + category.text.asOrderedText(), + -0x909396, + -0xcecfd1, + 0xCACACA + ) + textX += textWidth + 4 + } + super.render(matrices, mouseX, mouseY, delta) + } + + override fun onClose() { + client!!.setScreen(previousScreen) + } + + override fun updateSelectedEntry(widget: Any, entry: E?) { + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt index 44039f9..2a481bc 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt @@ -114,6 +114,7 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab 20, TranslatableText("modmanager.button.updateAll") ) { updateAll() }) + updateAllButtons.visible = false GlobalScope.launch { val provider = ModManager.modManager.getSelectedProvider() ?: return@launch @@ -123,7 +124,6 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab return@launch } is CategoriesResult.Success -> { - error = null categoryList.clear() if (ModManager.modManager.update.updates.isNotEmpty()) { categoryList.add(Category("updatable", TranslatableText("modmanager.category.updatable"))) @@ -268,7 +268,8 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab val update = ModManager.modManager.update.getUpdateForMod(selectedMod!!.mod) ?: return client?.setScreen(ModUpdateInfoScreen(this, update)) } - return // TODO: Open detail view + client?.setScreen(ModDetailScreen(this, selectedMod!!.mod)) + return } selectedMod = entry as ModListEntry return @@ -285,6 +286,9 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab override fun tick() { super.tick() + if (error != null) { + client!!.setScreen(ErrorScreen(previousScreen, this, error!!)) + } this.scrollPercentage = modList.scrollAmount this.searchField.tick() this.previousPage.active = page > 0 @@ -294,16 +298,8 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { this.renderBackground(matrices) - if (this.error != null) { - textRenderer.draw(matrices, TranslatableText("modmanager.error.title"), 10F, 35F, 0xFFFFFF) - val lines = textRenderer.wrapLines(error!!, width - 20) - for (line in lines) { - textRenderer.draw(matrices, line, 10F, 47F, 0xFFFFFF) - } - } else { - this.categoryList.render(matrices, mouseX, mouseY, delta) - this.modList.render(matrices, mouseX, mouseY, delta) - } + this.categoryList.render(matrices, mouseX, mouseY, delta) + this.modList.render(matrices, mouseX, mouseY, delta) this.searchField.render(matrices, mouseX, mouseY, delta) super.render(matrices, mouseX, mouseY, delta) } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.kt index bf073c4..f5c9c9e 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/widget/DescriptionWidget.kt @@ -33,7 +33,7 @@ class DescriptionWidget( bottom: Int, itemHeight: Int, parent: IListScreen, - val text: String + var text: String ) : ListWidget(client, width, height, top, bottom, itemHeight, parent) { private val textRenderer = MinecraftClient.getInstance().textRenderer diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/md/Markdown.kt b/src/main/kotlin/xyz/deathsgun/modmanager/md/Markdown.kt index 88171fc..44d2600 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/md/Markdown.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/md/Markdown.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package xyz.deathsgun.modmanager.md import net.minecraft.text.ClickEvent @@ -15,7 +31,8 @@ class Markdown(private var text: String) { private val imagePattern: Pattern = Pattern.compile("!\\[(.*?)]\\((.*?)\\)") fun toText(): List { - text = text.replace("\\u00A0", " ") + text = text.replace("\u00A0", " ") + text = text.replace("\r".toRegex(), "") text = text.replace("
".toRegex(), "\n").replace("
".toRegex(), "\n") val lines = text.split("\n").toTypedArray() diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt index 3131f2c..2518f60 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt @@ -64,6 +64,9 @@ class Modrinth : IModProvider, IModUpdateProvider { val response = this.http.send(request, HttpResponse.BodyHandlers.ofString()) val categories = json.decodeFromString>(response.body()) for (category in categories) { + if (category == "fabric") { // Fabric is not really a category + continue + } this.categories.add( Category( category, @@ -73,7 +76,7 @@ class Modrinth : IModProvider, IModUpdateProvider { } CategoriesResult.Success(this.categories) } catch (e: Exception) { - logger.error("Error while getting categories: ", e.message) + logger.error("Error while getting categories: {}", e.message) CategoriesResult.Error(TranslatableText("modmanager.error.failedToParse", e.message), e) } } @@ -140,7 +143,7 @@ class Modrinth : IModProvider, IModUpdateProvider { val result = json.decodeFromString(response.body()) ModsResult.Success(result.toList()) } catch (e: Exception) { - logger.error("Error while requesting mods", e.message) + logger.error("Error while requesting mods {}", e.message) ModsResult.Error(TranslatableText("modmanager.error.failedToParse", e.message)) } } @@ -157,7 +160,7 @@ class Modrinth : IModProvider, IModUpdateProvider { val response = try { this.http.send(request, HttpResponse.BodyHandlers.ofString()) } catch (e: Exception) { - logger.error("Error while getting mod", e.message) + logger.error("Error while getting mod {}", e.message) return ModResult.Error(TranslatableText("modmanager.error.network", e.message), e) } if (response.statusCode() != 200) { @@ -199,7 +202,7 @@ class Modrinth : IModProvider, IModUpdateProvider { val response = try { this.http.send(request, HttpResponse.BodyHandlers.ofString()) } catch (e: Exception) { - logger.error("Error while getting mod", e.message) + logger.error("Error while getting mod {}", e.message) return VersionResult.Error(TranslatableText("modmanager.error.network"), e) } if (response.statusCode() != 200) { diff --git a/src/main/resources/assets/modmanager/lang/en_us.json b/src/main/resources/assets/modmanager/lang/en_us.json index 6059736..be48a5d 100644 --- a/src/main/resources/assets/modmanager/lang/en_us.json +++ b/src/main/resources/assets/modmanager/lang/en_us.json @@ -3,6 +3,7 @@ "modmanager.badge.outdated": "Outdated", "modmanager.button.update": "Update", "modmanager.button.updateAll": "Update all", + "modmanager.button.tryAgain": "Try again", "modmanager.categories": "Categories", "modmanager.category.updatable": "Updatable Mods", "modmanager.category.technology": "Technology", @@ -21,7 +22,7 @@ "modmanager.changes.message": "Changes will take effect after a relaunch.\nQuit Minecraft now?", "modmanager.details.author": "By %s", "modmanager.details.versioning": "From %s to %s", - "modmanager.error.title": "§4Error:§r", + "modmanager.error.title": "§b§4Error:§r", "modmanager.error.unknown": "Unknown error while retrieving data", "modmanager.error.invalidStatus": "Received invalid status code {}", "modmanager.error.network": "Network error while retrieving data", From 31bdee6016042f777b095c4ac2f6de39d0e9447a Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Mon, 13 Sep 2021 21:33:45 +0200 Subject: [PATCH 23/25] Allow mods to be installed and removed Also added a progress screen while installing and updating mods Added some missing error messages Signed-off-by: DeathsGun --- .../modmanager/api/ModInstallResult.kt | 9 ++ .../modmanager/api/ModRemoveResult.kt | 8 + .../modmanager/gui/ModDetailScreen.kt | 51 ++++++ .../modmanager/gui/ModProgressScreen.kt | 149 +++++++++++++++++ .../modmanager/gui/ModUpdateInfoScreen.kt | 28 +--- .../modmanager/gui/ModsOverviewScreen.kt | 90 ++++------- .../modmanager/models/FabricMetadata.kt | 10 +- .../modmanager/providers/modrinth/Modrinth.kt | 4 +- .../modmanager/update/UpdateManager.kt | 152 +++++++++++++----- .../assets/modmanager/lang/en_us.json | 18 ++- 10 files changed, 386 insertions(+), 133 deletions(-) create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/api/ModInstallResult.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/api/ModRemoveResult.kt create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/gui/ModProgressScreen.kt diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/ModInstallResult.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/ModInstallResult.kt new file mode 100644 index 0000000..582938a --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/ModInstallResult.kt @@ -0,0 +1,9 @@ +package xyz.deathsgun.modmanager.api + +import net.minecraft.text.TranslatableText + +sealed class ModInstallResult { + + object Success : ModInstallResult() + data class Error(val text: TranslatableText, val cause: Exception? = null) : ModInstallResult() +} diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/ModRemoveResult.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/ModRemoveResult.kt new file mode 100644 index 0000000..23bdda9 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/ModRemoveResult.kt @@ -0,0 +1,8 @@ +package xyz.deathsgun.modmanager.api + +import net.minecraft.text.TranslatableText + +sealed class ModRemoveResult { + object Success : ModRemoveResult() + data class Error(val text: TranslatableText, val cause: Exception? = null) : ModRemoveResult() +} diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModDetailScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModDetailScreen.kt index 791b427..39f61b6 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModDetailScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModDetailScreen.kt @@ -24,14 +24,17 @@ import net.minecraft.client.gui.widget.ButtonWidget import net.minecraft.client.util.math.MatrixStack import net.minecraft.text.* import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.api.ModRemoveResult import xyz.deathsgun.modmanager.api.gui.list.IListScreen import xyz.deathsgun.modmanager.api.http.ModResult import xyz.deathsgun.modmanager.api.mod.Mod import xyz.deathsgun.modmanager.gui.widget.DescriptionWidget +import xyz.deathsgun.modmanager.state.ModState class ModDetailScreen(private val previousScreen: Screen, var mod: Mod) : Screen(LiteralText(mod.name)), IListScreen { + private lateinit var actionButton: ButtonWidget private lateinit var descriptionWidget: DescriptionWidget init { @@ -70,7 +73,55 @@ class ModDetailScreen(private val previousScreen: Screen, var mod: Mod) : Screen addDrawableChild(ButtonWidget(buttonX, height - 28, 150, 20, ScreenTexts.BACK) { client!!.setScreen(previousScreen) }) + this.actionButton = addDrawableChild( + ButtonWidget( + this.width - buttonX - 150, + this.height - 28, + 150, + 20, + TranslatableText("modmanager.button.install") + ) { + when (ModManager.modManager.getModState(mod.id)) { + ModState.DOWNLOADABLE -> { + client!!.setScreen( + ModProgressScreen( + mod, + ModProgressScreen.Action.INSTALL, + previousScreen, + this + ) + ) + } + ModState.OUTDATED -> { + client!!.setScreen( + ModProgressScreen( + mod, + ModProgressScreen.Action.UPDATE, + previousScreen, + this + ) + ) + } + ModState.INSTALLED -> { + removeMod() + } + } + }) + } + + private fun removeMod() { + when (val result = ModManager.modManager.update.removeMod(mod)) { + is ModRemoveResult.Success -> client!!.setScreen(previousScreen) + is ModRemoveResult.Error -> client!!.setScreen(ErrorScreen(previousScreen, this, result.text)) + } + } + override fun tick() { + actionButton.message = when (ModManager.modManager.getModState(mod.id)) { + ModState.INSTALLED -> TranslatableText("modmanager.button.remove") + ModState.DOWNLOADABLE -> TranslatableText("modmanager.button.install") + ModState.OUTDATED -> TranslatableText("modmanager.button.update") + } } override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModProgressScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModProgressScreen.kt new file mode 100644 index 0000000..7d59f97 --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModProgressScreen.kt @@ -0,0 +1,149 @@ +package xyz.deathsgun.modmanager.gui + +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import net.minecraft.client.MinecraftClient +import net.minecraft.client.font.MultilineText +import net.minecraft.client.gui.hud.BackgroundHelper.ColorMixer +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screen.ScreenTexts +import net.minecraft.client.gui.widget.ButtonWidget +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.text.LiteralText +import net.minecraft.text.Text +import net.minecraft.text.TranslatableText +import net.minecraft.util.math.MathHelper +import org.apache.logging.log4j.LogManager +import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.api.ModInstallResult +import xyz.deathsgun.modmanager.api.ModUpdateResult +import xyz.deathsgun.modmanager.api.mod.Mod +import kotlin.math.roundToInt + +class ModProgressScreen( + val mod: Mod, + private val action: Action, + private val previousScreen: Screen, + private val infoScreen: Screen +) : + Screen(LiteralText("")) { + + private var status: MultilineText? = null + private lateinit var backButton: ButtonWidget + private var finished: Boolean = false + private var pos = 0 + private var rightEnd = 0 + private var leftEnd = 0 + + @OptIn(DelicateCoroutinesApi::class) + override fun init() { + backButton = addDrawableChild( + ButtonWidget( + width / 2 - 75, + (height * 0.6 + 40).roundToInt(), + 150, + 20, + ScreenTexts.BACK + ) { + client!!.setScreen(previousScreen) + }) + backButton.visible = false + GlobalScope.launch { + when (action) { + Action.UPDATE -> updateMod() + Action.INSTALL -> installMod() + } + } + } + + override fun tick() { + pos += 5 + } + + override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { + rightEnd = width - width / 8 + leftEnd = width / 8 + super.renderBackground(matrices) + + var y = (height * 0.60).roundToInt() + if (status != null) { + val linesHeight = this.status!!.count() * 9 + y = MathHelper.clamp(90 + linesHeight + 12, this.height / 6 + 69, this.height - 24) + status!!.drawCenterWithShadow(matrices, this.width / 2, 90) + } + if (!finished) { + renderProgressBar(matrices, width / 8, y - 5, rightEnd, y + 5) + } + + backButton.y = y + 10 + + super.render(matrices, mouseX, mouseY, delta) + } + + private fun renderProgressBar(matrices: MatrixStack?, minX: Int, minY: Int, maxX: Int, maxY: Int) { + val color = ColorMixer.getArgb(255, 255, 255, 255) + var barWidth = width / 10 + val overlap = (minX + pos + barWidth) - maxX + 2 + if (overlap > 0) { + barWidth -= overlap + } + if ((minX + pos) - maxX + 2 > 0) { + pos = 0 + } + fill(matrices, minX + 2 + pos, minY + 2, minX + pos + barWidth, maxY - 2, color) + fill(matrices, minX + 1, minY, maxX - 1, minY + 1, color) + fill(matrices, minX + 1, maxY, maxX - 1, maxY - 1, color) + fill(matrices, minX, minY, minX + 1, maxY, color) + fill(matrices, maxX, minY, maxX - 1, maxY, color) + } + + + private fun installMod() { + setStatus(TranslatableText("modmanager.status.installing", mod.name)) + when (val result = ModManager.modManager.update.installMod(mod)) { + is ModInstallResult.Error -> { + LogManager.getLogger().error(result.text.key, result.cause) + client!!.send { + MinecraftClient.getInstance().setScreen(ErrorScreen(previousScreen, infoScreen, result.text)) + } + } + is ModInstallResult.Success -> { + finished = true + this.backButton.visible = true + setStatus(TranslatableText("modmanager.status.install.success", mod.name)) + } + } + } + + private fun updateMod() { + setStatus(TranslatableText("modmanager.status.updating", mod.name)) + val update = ModManager.modManager.update.getUpdateForMod(mod) ?: return + when (val result = ModManager.modManager.update.updateMod(update)) { + is ModUpdateResult.Error -> { + LogManager.getLogger().error(result.text.key, result.cause) + client!!.send { + MinecraftClient.getInstance().setScreen(ErrorScreen(previousScreen, infoScreen, result.text)) + } + } + is ModUpdateResult.Success -> { + finished = true + this.backButton.visible = true + setStatus(TranslatableText("modmanager.status.update.success", mod.name)) + } + } + } + + fun setStatus(text: Text) { + status = MultilineText.create(textRenderer, text, width - 2 * (width / 8)) + } + + override fun onClose() { + client!!.setScreen(previousScreen) + } + + enum class Action { + INSTALL, UPDATE + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt index 52df163..0840910 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt @@ -18,19 +18,13 @@ package xyz.deathsgun.modmanager.gui import com.mojang.blaze3d.systems.RenderSystem import com.terraformersmc.modmenu.util.DrawingUtil -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.DrawableHelper import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.screen.ScreenTexts import net.minecraft.client.gui.widget.ButtonWidget import net.minecraft.client.util.math.MatrixStack import net.minecraft.text.* -import org.apache.logging.log4j.LogManager import xyz.deathsgun.modmanager.ModManager -import xyz.deathsgun.modmanager.api.ModUpdateResult import xyz.deathsgun.modmanager.api.gui.list.IListScreen import xyz.deathsgun.modmanager.gui.widget.DescriptionWidget import xyz.deathsgun.modmanager.update.Update @@ -64,30 +58,10 @@ class ModUpdateInfoScreen(private val previousScreen: Screen, private val update updateButtonWidget = addDrawableChild(ButtonWidget( this.width - buttonX - 150, this.height - 25, 150, 20, TranslatableText("modmanager.button.update") ) { - installUpdate() + client!!.setScreen(ModProgressScreen(update.mod, ModProgressScreen.Action.UPDATE, previousScreen, this)) }) } - @OptIn(DelicateCoroutinesApi::class) - private fun installUpdate() { - this.updateButtonWidget.active = false - this.updateButtonWidget.message = TranslatableText("modmanager.status.updating") - GlobalScope.launch { - when (val result = ModManager.modManager.update.updateMod(update)) { - is ModUpdateResult.Error -> { - this@ModUpdateInfoScreen.updateButtonWidget.active = true - this@ModUpdateInfoScreen.updateButtonWidget.message = TranslatableText("modmanager.button.update") - LogManager.getLogger().error(result.text.key, result.cause) - } - is ModUpdateResult.Success -> { - client!!.send { - MinecraftClient.getInstance().setScreen(previousScreen) - } - } - } - } - } - override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { renderBackground(matrices) descriptionWidget.render(matrices, mouseX, mouseY, delta) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt index 2a481bc..a703f24 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt @@ -28,7 +28,6 @@ import net.minecraft.client.util.math.MatrixStack import net.minecraft.text.TranslatableText import org.lwjgl.glfw.GLFW import xyz.deathsgun.modmanager.ModManager -import xyz.deathsgun.modmanager.api.ModUpdateResult import xyz.deathsgun.modmanager.api.gui.list.IListScreen import xyz.deathsgun.modmanager.api.http.CategoriesResult import xyz.deathsgun.modmanager.api.http.ModsResult @@ -54,7 +53,6 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab private lateinit var categoryList: CategoryListWidget private lateinit var nextPage: ButtonWidget private lateinit var previousPage: ButtonWidget - private lateinit var updateAllButtons: ButtonWidget @OptIn(DelicateCoroutinesApi::class) override fun init() { @@ -107,15 +105,6 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab TranslatableText("modmanager.page.next") ) { showNextPage() }) - updateAllButtons = addDrawableChild(ButtonWidget( - width - 10 - 160, - 10, - 160, - 20, - TranslatableText("modmanager.button.updateAll") - ) { updateAll() }) - updateAllButtons.visible = false - GlobalScope.launch { val provider = ModManager.modManager.getSelectedProvider() ?: return@launch when (val result = provider.getCategories()) { @@ -134,21 +123,12 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab } if (selectedCategory != null) { showModsByCategory() + categoryList.setSelected(selectedCategory) modList.scrollAmount = scrollPercentage return@launch } if (query.isNotEmpty()) { - when (val result = provider.search(query, page, limit)) { - is ModsResult.Error -> { - error = result.text - return@launch - } - is ModsResult.Success -> { - error = null - modList.setMods(result.mods) - modList.scrollAmount = scrollPercentage - } - } + showModsBySearch() return@launch } categoryList.setSelectedByIndex(0) @@ -156,32 +136,15 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab modList.init() categoryList.init() //TODO: Sorting selector - //TODO: Paging - } - - @OptIn(DelicateCoroutinesApi::class) - private fun updateAll() { - this.updateAllButtons.active = false - this.updateAllButtons.message = TranslatableText("modmanager.status.updating") - GlobalScope.launch { - val updates = ModManager.modManager.update.updates - for (update in ArrayList(updates)) { - when (val result = ModManager.modManager.update.updateMod(update)) { - is ModUpdateResult.Error -> error = result.text - } - } - if (error == null) { - updateAllButtons.active = true - updateAllButtons.message = TranslatableText("modmanager.button.update") - modList.clear() - updateAllButtons.visible = false - return@launch - } - } } private fun showNextPage() { this.page++ + modList.scrollAmount = 0.0 + if (query.isNotBlank()) { + showModsBySearch() + return + } if (selectedCategory == null) { showModsByRelevance() return @@ -191,9 +154,14 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab private fun showPreviousPage() { this.page-- + modList.scrollAmount = 0.0 if (this.page < 0) { page = 0 } + if (query.isNotBlank()) { + showModsBySearch() + return + } if (selectedCategory == null) { showModsByRelevance() return @@ -214,7 +182,6 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab } } - @OptIn(DelicateCoroutinesApi::class) private fun showModsByCategory() { selectedCategory ?: return val provider = ModManager.modManager.getSelectedProvider() ?: return @@ -238,22 +205,27 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { if (this.searchField.isFocused && keyCode == GLFW.GLFW_KEY_ENTER) { - val provider = ModManager.modManager.getSelectedProvider() ?: return true - return when (val result = provider.search(this.query, 0, 20)) { - is ModsResult.Error -> { - this.error = result.text - true - } - is ModsResult.Success -> { - this.error = null - this.modList.setMods(result.mods) - true - } - } + page = 0 + this.modList.scrollAmount = 0.0 + showModsBySearch() + return true } return super.keyPressed(keyCode, scanCode, modifiers) } + private fun showModsBySearch() { + val provider = ModManager.modManager.getSelectedProvider() ?: return + when (val result = provider.search(this.query, page, limit)) { + is ModsResult.Error -> { + this.error = result.text + } + is ModsResult.Success -> { + this.error = null + this.modList.setMods(result.mods) + } + } + } + override fun getFocused(): Element? { return super.getFocused() } @@ -267,11 +239,13 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab if (selectedCategory?.id == "updatable") { val update = ModManager.modManager.update.getUpdateForMod(selectedMod!!.mod) ?: return client?.setScreen(ModUpdateInfoScreen(this, update)) + return } client?.setScreen(ModDetailScreen(this, selectedMod!!.mod)) return } selectedMod = entry as ModListEntry + query = "" return } if (widget is CategoryListWidget) { @@ -280,6 +254,7 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab } page = 0 selectedCategory = entry as CategoryListEntry + query = "" showModsByCategory() } } @@ -293,7 +268,6 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab this.searchField.tick() this.previousPage.active = page > 0 this.nextPage.active = this.modList.getElementCount() >= limit - this.updateAllButtons.visible = selectedCategory?.id == "updatable" && this.modList.getElementCount() > 0 } override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/models/FabricMetadata.kt b/src/main/kotlin/xyz/deathsgun/modmanager/models/FabricMetadata.kt index 2500583..c1c06df 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/models/FabricMetadata.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/models/FabricMetadata.kt @@ -20,5 +20,11 @@ import kotlinx.serialization.Serializable @Serializable data class FabricMetadata( - val id: String -) + val id: String, + val custom: Custom = Custom(emptyMap()) +) { + @Serializable + data class Custom( + val modmanager: Map = emptyMap() + ) +} diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt index 2518f60..57171c5 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt @@ -111,7 +111,7 @@ class Modrinth : IModProvider, IModUpdateProvider { } result } catch (e: Exception) { - ModsResult.Error(TranslatableText("modmanager.error.unknown", e), e) + ModsResult.Error(TranslatableText("modmanager.error.unknown", e.message), e) } } @@ -203,7 +203,7 @@ class Modrinth : IModProvider, IModUpdateProvider { this.http.send(request, HttpResponse.BodyHandlers.ofString()) } catch (e: Exception) { logger.error("Error while getting mod {}", e.message) - return VersionResult.Error(TranslatableText("modmanager.error.network"), e) + return VersionResult.Error(TranslatableText("modmanager.error.network", e.message), e) } if (response.statusCode() != 200) { return VersionResult.Error(TranslatableText("modmanager.error.invalidStatus", response.statusCode())) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt index fec578c..bcf096d 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt @@ -28,8 +28,11 @@ import net.fabricmc.loader.api.metadata.ModMetadata import net.fabricmc.loader.util.version.VersionDeserializer import net.minecraft.text.TranslatableText import net.minecraft.util.Util +import org.apache.commons.io.FileUtils import org.apache.logging.log4j.LogManager import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.api.ModInstallResult +import xyz.deathsgun.modmanager.api.ModRemoveResult import xyz.deathsgun.modmanager.api.ModUpdateResult import xyz.deathsgun.modmanager.api.http.ModResult import xyz.deathsgun.modmanager.api.http.ModsResult @@ -49,7 +52,6 @@ import java.nio.file.Path import java.security.MessageDigest import java.time.Duration import java.util.concurrent.TimeUnit -import java.util.stream.Collectors import java.util.zip.ZipFile import kotlin.io.path.name @@ -61,6 +63,8 @@ class UpdateManager { private val http: HttpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(15)).build() val updates = ArrayList() + //region Update Checking + suspend fun checkUpdates() = coroutineScope { val mods = getCheckableMods() mods.forEach { metadata -> @@ -118,7 +122,12 @@ class UpdateManager { return } val mod = - (queryResult as ModsResult.Success).mods.find { mod -> mod.slug == metadata.id || mod.name == metadata.name } + (queryResult as ModsResult.Success).mods.find { mod -> + mod.slug == metadata.id || mod.name.equals( + metadata.name, + true + ) + } if (mod == null) { logger.warn("Error while searching for fallback id for mod {}: No possible match found", metadata.id) ModManager.modManager.setModState(metadata.id, metadata.id, ModState.INSTALLED) @@ -191,6 +200,62 @@ class UpdateManager { fun getUpdateForMod(mod: Mod): Update? { return this.updates.find { it.mod.id == mod.id || it.fabricId == mod.slug } } + //endregion + + fun installMod(mod: Mod): ModInstallResult { + return try { + val provider = ModManager.modManager.getSelectedProvider() + ?: return ModInstallResult.Error(TranslatableText("modmanager.error.noProviderSelected")) + logger.info("Installing {}", mod.name) + val versions = when (val result = provider.getVersionsForMod(mod.id)) { + is VersionResult.Error -> return ModInstallResult.Error(result.text, result.cause) + is VersionResult.Success -> result.versions + } + val version = findLatestCompatible("0.0.0.0", versions) + ?: return ModInstallResult.Error(TranslatableText("modmanager.error.noCompatibleModVersionFound")) + + val dir = FabricLoader.getInstance().gameDir.resolve("mods") + when (val result = installVersion(mod, version, dir)) { + is ModUpdateResult.Success -> ModInstallResult.Success + is ModUpdateResult.Error -> ModInstallResult.Error(result.text, result.cause) + } + } catch (e: Exception) { + ModInstallResult.Error(TranslatableText("")) + } + } + + private fun installVersion(mod: Mod, version: Version, dir: Path, fabricId: String = mod.slug): ModUpdateResult { + return try { + val asset = + version.assets.find { (it.filename.endsWith(".jar") || it.primary) && !it.filename.contains("forge") } + ?: return ModUpdateResult.Error(TranslatableText("modmanager.error.update.noFabricJar")) + val jar = dir.resolve(asset.filename) // Download into same directory where the old jar was + val request = HttpRequest.newBuilder(URI.create(asset.url)).GET() + .setHeader("User-Agent", "ModManager ${ModManager.getVersion()}").build() + val response = this.http.send(request, HttpResponse.BodyHandlers.ofFile(jar)) + if (response.statusCode() != 200) { + ModUpdateResult.Error(TranslatableText("modmanager.error.invalidStatus", response.statusCode())) + } + val expected = asset.hashes["sha512"] + val calculated = jar.sha512() + if (calculated != expected) { + return ModUpdateResult.Error( + TranslatableText( + "modmanager.error.invalidHash", + "SHA-512", + expected, + calculated + ) + ) + } + ModManager.modManager.setModState(fabricId, mod.id, ModState.INSTALLED) + this.updates.removeIf { it.fabricId == mod.slug || it.mod.id == mod.id } + ModManager.modManager.changed = true + ModUpdateResult.Success + } catch (e: Exception) { + ModUpdateResult.Error(TranslatableText("modmanager.error.unknown.update", e)) + } + } private fun findLatestCompatible(installedVersion: String, versions: List): Version? { var latest: Version? = null @@ -203,8 +268,12 @@ class UpdateManager { ) { continue } - val ver = + val ver = try { VersionDeserializer.deserializeSemantic(version.version.split("+")[0]) // Remove additional info from version + } catch (e: Exception) { + logger.warn("Skipping error producing version {}", version.version) + continue + } if (latestVersion == null || ver > latestVersion) { latest = version latestVersion = ver @@ -221,43 +290,13 @@ class UpdateManager { ?: return ModUpdateResult.Error(TranslatableText("modmanager.error.container.notFound")) val oldJar = findJarByModContainer(oldUpdate.metadata) ?: return ModUpdateResult.Error(TranslatableText("modmanager.error.jar.notFound")) + logger.info("Updating {}", update.mod.name) try { oldJar.forceDelete() } catch (e: Exception) { return ModUpdateResult.Error(TranslatableText("modmanager.error.jar.failedDelete", e)) } - val asset = - update.version.assets.find { (it.filename.endsWith(".jar") || it.primary) && !it.filename.contains("forge") } - ?: return ModUpdateResult.Error(TranslatableText("modmanager.error.update.noFabricJar")) - val newJar = oldJar.resolveSibling(asset.filename) // Download into same directory where the old jar was - - val request = HttpRequest.newBuilder(URI.create(asset.url)).GET() - .setHeader("User-Agent", "ModManager ${ModManager.getVersion()}").build() - return try { - val response = this.http.send(request, HttpResponse.BodyHandlers.ofFile(newJar)) - if (response.statusCode() != 200) { - ModUpdateResult.Error(TranslatableText("modmanager.error.invalidStatus", response.statusCode())) - } - val expected = asset.hashes["sha512"] - val calculated = newJar.sha512() - if (calculated != expected) { - return ModUpdateResult.Error( - TranslatableText( - "modmanager.error.invalidHash", - "SHA-512", - expected, - calculated - ) - ) - } - ModManager.modManager.setModState(update.fabricId, update.mod.id, ModState.INSTALLED) - this.updates.removeIf { it.fabricId == update.fabricId || it.mod.id == update.mod.id } - ModManager.modManager.changed = true - ModUpdateResult.Success - } catch (e: Exception) { - ModUpdateResult.Error(TranslatableText("modmanager.error.update.unknown", e)) - } - + return installVersion(update.mod, update.version, oldJar.parent, update.fabricId) } private val json = Json { @@ -266,17 +305,40 @@ class UpdateManager { @OptIn(ExperimentalSerializationApi::class) fun findJarByModContainer(container: ModMetadata): Path? { - val jars = Files.list(FabricLoader.getInstance().gameDir.resolve("mods")) - .filter { it.name.endsWith(".jar") }.collect(Collectors.toList()) + val jars = + FileUtils.listFiles(FabricLoader.getInstance().gameDir.resolve("mods").toFile(), arrayOf("jar"), true) return try { for (jar in jars) { - val jarFile = ZipFile(jar.toFile()) + val jarFile = ZipFile(jar) val fabricEntry = jarFile.getEntry("fabric.mod.json") val data = jarFile.getInputStream(fabricEntry).bufferedReader().use { it.readText() } val meta = json.decodeFromString(data) jarFile.close() if (meta.id == container.id) { - return jar + return jar.toPath() + } + } + null + } catch (e: Exception) { + null + } + } + + @OptIn(ExperimentalSerializationApi::class) + fun findJarByMod(mod: Mod): Path? { + val jars = + FileUtils.listFiles(FabricLoader.getInstance().gameDir.resolve("mods").toFile(), arrayOf("jar"), true) + return try { + for (jar in jars) { + val jarFile = ZipFile(jar) + val fabricEntry = jarFile.getEntry("fabric.mod.json") + val data = jarFile.getInputStream(fabricEntry).bufferedReader().use { it.readText() } + val meta = json.decodeFromString(data) + jarFile.close() + if (meta.id == mod.id || meta.id == mod.slug || meta.id == mod.slug.replace("-", "") || + meta.custom.modmanager[ModManager.modManager.config.defaultProvider] == mod.id + ) { + return jar.toPath() } } null @@ -325,4 +387,16 @@ class UpdateManager { } return hashText } + + fun removeMod(mod: Mod): ModRemoveResult { + val jar = findJarByMod(mod) + ?: return ModRemoveResult.Error(TranslatableText("modmanager.error.jar.notFound")) + return try { + jar.forceDelete() + ModManager.modManager.setModState(mod.slug, mod.id, ModState.DOWNLOADABLE) + ModRemoveResult.Success + } catch (e: Exception) { + return ModRemoveResult.Error(TranslatableText("modmanager.error.jar.failedDelete", e)) + } + } } \ No newline at end of file diff --git a/src/main/resources/assets/modmanager/lang/en_us.json b/src/main/resources/assets/modmanager/lang/en_us.json index be48a5d..4d2fd6b 100644 --- a/src/main/resources/assets/modmanager/lang/en_us.json +++ b/src/main/resources/assets/modmanager/lang/en_us.json @@ -2,6 +2,8 @@ "modmanager.badge.installed": "Installed", "modmanager.badge.outdated": "Outdated", "modmanager.button.update": "Update", + "modmanager.button.install": "Install", + "modmanager.button.remove": "Remove", "modmanager.button.updateAll": "Update all", "modmanager.button.tryAgain": "Try again", "modmanager.categories": "Categories", @@ -23,11 +25,17 @@ "modmanager.details.author": "By %s", "modmanager.details.versioning": "From %s to %s", "modmanager.error.title": "§b§4Error:§r", - "modmanager.error.unknown": "Unknown error while retrieving data", - "modmanager.error.invalidStatus": "Received invalid status code {}", - "modmanager.error.network": "Network error while retrieving data", - "modmanager.error.failedToParse": "Failed to parse response", - "modmanager.status.updating": "Updating...", + "modmanager.error.unknown": "Unknown error while retrieving data:\n %s", + "modmanager.error.unknown.install": "Unknown error while during installation:\n %s", + "modmanager.error.unknown.update": "Unknown error while during update:\n %s", + "modmanager.error.invalidStatus": "Received invalid status code:\n %d", + "modmanager.error.network": "Network error while retrieving data:\n %s", + "modmanager.error.failedToParse": "Failed to parse response:\n %s", + "modmanager.error.noCompatibleModVersionFound": "No compatible version found for your current channel!\nTry changing the channel in the Mod Manager settings!", + "modmanager.status.installing": "Installing %s...", + "modmanager.status.install.success": "Successfully installed %s!\nDo you want to go back?", + "modmanager.status.updating": "Updating %s...", + "modmanager.status.update.success": "Successfully updated %s!\nDo you want to go back?", "modmanager.search": "Search...", "modmanager.page.next": "Next Page", "modmanager.page.previous": "Previous Page", From 3e317860d53b60e48e5e9518ee61e47b2da3e1da Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Wed, 15 Sep 2021 10:11:24 +0200 Subject: [PATCH 24/25] Added sorting Signed-off-by: DeathsGun --- .../modmanager/api/provider/IModProvider.kt | 4 +-- .../modmanager/api/provider/Sorting.kt | 8 +++++ .../modmanager/gui/ModsOverviewScreen.kt | 29 +++++++++++-------- .../modmanager/providers/modrinth/Modrinth.kt | 28 +++++------------- .../modmanager/update/UpdateManager.kt | 3 +- .../assets/modmanager/lang/en_us.json | 5 ++++ 6 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt index 97f4b74..4408324 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt @@ -50,7 +50,7 @@ interface IModProvider : IModUpdateProvider { * @param limit to not overfill the ui and for shorter loading times the amount of returned mods needs to limited * @return a list of sorted mods */ - fun getMods(category: Category, page: Int, limit: Int): ModsResult + fun getMods(category: Category, sorting: Sorting, page: Int, limit: Int): ModsResult /** * Returns a limited number of [Mod]'s from a given search. @@ -60,7 +60,7 @@ interface IModProvider : IModUpdateProvider { * @param limit the amount of mods to return * @return a list of mods matching the search term */ - fun search(query: String, page: Int, limit: Int): ModsResult + fun search(query: String, sorting: Sorting, page: Int, limit: Int): ModsResult /** * Returns a more detailed representation of the mod diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/Sorting.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/Sorting.kt index 06acc90..0ad2171 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/Sorting.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/provider/Sorting.kt @@ -15,6 +15,14 @@ */ package xyz.deathsgun.modmanager.api.provider +import net.minecraft.text.Text +import net.minecraft.text.TranslatableText + enum class Sorting { RELEVANCE, DOWNLOADS, UPDATED, NEWEST; + + fun translations(): Text { + return TranslatableText(String.format("modmanager.sorting.%s", name.lowercase())) + } + } \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt index a703f24..35c1a53 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt @@ -23,6 +23,7 @@ import net.minecraft.client.gui.Element import net.minecraft.client.gui.screen.ConfirmScreen import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.widget.ButtonWidget +import net.minecraft.client.gui.widget.CyclingButtonWidget import net.minecraft.client.gui.widget.TextFieldWidget import net.minecraft.client.util.math.MatrixStack import net.minecraft.text.TranslatableText @@ -49,6 +50,8 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab private var scrollPercentage: Double = 0.0 private lateinit var searchField: TextFieldWidget private var error: TranslatableText? = null + private var sorting: Sorting = Sorting.RELEVANCE + private lateinit var sortingButtonWidget: CyclingButtonWidget private lateinit var modList: ModListWidget private lateinit var categoryList: CategoryListWidget private lateinit var nextPage: ButtonWidget @@ -69,6 +72,13 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab ) searchField.setChangedListener { this.query = it } + sortingButtonWidget = addDrawableChild( + CyclingButtonWidget.builder(Sorting::translations) + .values(Sorting.RELEVANCE, Sorting.DOWNLOADS, Sorting.NEWEST, Sorting.UPDATED) + .build(180, 10, 120, 20, TranslatableText("modmanager.sorting.sort")) + { _: CyclingButtonWidget, sorting: Sorting -> this.sorting = sorting; updateModList() } + ) + categoryList = addSelectableChild( CategoryListWidget( client!!, @@ -135,21 +145,12 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab } modList.init() categoryList.init() - //TODO: Sorting selector } private fun showNextPage() { this.page++ modList.scrollAmount = 0.0 - if (query.isNotBlank()) { - showModsBySearch() - return - } - if (selectedCategory == null) { - showModsByRelevance() - return - } - showModsByCategory() + updateModList() } private fun showPreviousPage() { @@ -158,6 +159,10 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab if (this.page < 0) { page = 0 } + updateModList() + } + + private fun updateModList() { if (query.isNotBlank()) { showModsBySearch() return @@ -192,7 +197,7 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab } return } - when (val result = provider.getMods(selectedCategory!!.category, page, limit)) { + when (val result = provider.getMods(selectedCategory!!.category, sorting, page, limit)) { is ModsResult.Error -> { error = result.text } @@ -215,7 +220,7 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab private fun showModsBySearch() { val provider = ModManager.modManager.getSelectedProvider() ?: return - when (val result = provider.search(this.query, page, limit)) { + when (val result = provider.search(this.query, sorting, page, limit)) { is ModsResult.Error -> { this.error = result.text } diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt index 57171c5..61c88dd 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/providers/modrinth/Modrinth.kt @@ -45,7 +45,6 @@ class Modrinth : IModProvider, IModUpdateProvider { private val logger = LogManager.getLogger("Modrinth") private val categories: ArrayList = ArrayList() - private val cache: HashMap = HashMap() private val baseUri = "https://api.modrinth.com" private val http = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build() @@ -83,19 +82,11 @@ class Modrinth : IModProvider, IModUpdateProvider { override fun getMods(sorting: Sorting, page: Int, limit: Int): ModsResult { val builder = URIBuilder("${baseUri}/api/v1/mod") - builder.addParameter("index", sorting.name.lowercase()) - builder.addParameter("filters", "categories=\"fabric\"") - return getMods(builder, page, limit) + builder.addParameter("filters", "categories=\"fabric\" AND NOT client_side=\"unsupported\"") + return getMods(builder, sorting, page, limit) } - override fun getMods(category: Category, page: Int, limit: Int): ModsResult { - val key = java.lang.String.format("%s|%d|%d", category.id, page, limit) - if (this.cache.containsKey(key)) { - val cachedResult = this.cache[key]!! - if (cachedResult is ModsResult.Success) { - return cachedResult - } - } + override fun getMods(category: Category, sorting: Sorting, page: Int, limit: Int): ModsResult { val builder = URIBuilder("${baseUri}/api/v1/mod") builder.addParameter( "filters", @@ -105,32 +96,29 @@ class Modrinth : IModProvider, IModUpdateProvider { ) ) return try { - val result = getMods(builder, page, limit) - if (result is ModsResult.Success) { - this.cache[key] = result - } - result + getMods(builder, sorting, page, limit) } catch (e: Exception) { ModsResult.Error(TranslatableText("modmanager.error.unknown", e.message), e) } } - override fun search(query: String, page: Int, limit: Int): ModsResult { + override fun search(query: String, sorting: Sorting, page: Int, limit: Int): ModsResult { val builder = URIBuilder("${baseUri}/api/v1/mod") builder.addParameter("query", query) builder.addParameter("filters", "categories=\"fabric\" AND NOT client_side=\"unsupported\"") return try { - getMods(builder, page, limit) + getMods(builder, sorting, page, limit) } catch (e: Exception) { ModsResult.Error(TranslatableText("modmanager.error.unknown", e.message), e) } } - private fun getMods(builder: URIBuilder, page: Int, limit: Int): ModsResult { + private fun getMods(builder: URIBuilder, sorting: Sorting, page: Int, limit: Int): ModsResult { builder.addParameter( "version", String.format("versions=%s", ModManager.getMinecraftVersion()) ) + builder.addParameter("index", sorting.name.lowercase()) builder.addParameter("offset", (page * limit).toString()) builder.addParameter("limit", limit.toString()) val request = HttpRequest.newBuilder().GET().setHeader("User-Agent", "ModManager " + ModManager.getVersion()) diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt index bcf096d..1b6c4fc 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/update/UpdateManager.kt @@ -40,6 +40,7 @@ import xyz.deathsgun.modmanager.api.http.VersionResult import xyz.deathsgun.modmanager.api.mod.Mod import xyz.deathsgun.modmanager.api.mod.Version import xyz.deathsgun.modmanager.api.provider.IModUpdateProvider +import xyz.deathsgun.modmanager.api.provider.Sorting import xyz.deathsgun.modmanager.models.FabricMetadata import xyz.deathsgun.modmanager.state.ModState import java.math.BigInteger @@ -115,7 +116,7 @@ class UpdateManager { return } - val queryResult = provider.search(metadata.name, 0, 10) + val queryResult = provider.search(metadata.name, Sorting.RELEVANCE, 0, 10) if (queryResult is ModsResult.Error) { logger.warn("Error while searching for fallback id for mod {}: ", metadata.id, queryResult.cause) ModManager.modManager.setModState(metadata.id, metadata.id, ModState.INSTALLED) diff --git a/src/main/resources/assets/modmanager/lang/en_us.json b/src/main/resources/assets/modmanager/lang/en_us.json index 4d2fd6b..82ca5dc 100644 --- a/src/main/resources/assets/modmanager/lang/en_us.json +++ b/src/main/resources/assets/modmanager/lang/en_us.json @@ -36,6 +36,11 @@ "modmanager.status.install.success": "Successfully installed %s!\nDo you want to go back?", "modmanager.status.updating": "Updating %s...", "modmanager.status.update.success": "Successfully updated %s!\nDo you want to go back?", + "modmanager.sorting.sort": "Sort by", + "modmanager.sorting.relevance": "Relevance", + "modmanager.sorting.downloads": "Downloads", + "modmanager.sorting.updated": "Updated", + "modmanager.sorting.newest": "Newest", "modmanager.search": "Search...", "modmanager.page.next": "Next Page", "modmanager.page.previous": "Previous Page", From 891a1d45dded7614f2885b819a39feed96c78b20 Mon Sep 17 00:00:00 2001 From: DeathsGun Date: Wed, 15 Sep 2021 12:03:47 +0200 Subject: [PATCH 25/25] Add config screen Signed-off-by: DeathsGun --- .../modmanager/ModMenuEntrypoint.java | 28 +++++++ .../modmanager/mixin/ModsScreenMixin.java | 16 ++++ .../modmanager/api/ModInstallResult.kt | 16 ++++ .../modmanager/api/ModRemoveResult.kt | 16 ++++ .../modmanager/api/ModUpdateResult.kt | 16 ++++ .../xyz/deathsgun/modmanager/config/Config.kt | 10 ++- .../deathsgun/modmanager/gui/ConfigScreen.kt | 78 +++++++++++++++++++ .../modmanager/gui/ModProgressScreen.kt | 16 ++++ .../deathsgun/modmanager/state/ModState.kt | 16 ++++ .../deathsgun/modmanager/state/SavedState.kt | 16 ++++ .../assets/modmanager/lang/en_us.json | 8 ++ src/main/resources/fabric.mod.json | 3 + .../providers/modrinth/ModrinthTest.kt | 4 +- 13 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 src/main/java/xyz/deathsgun/modmanager/ModMenuEntrypoint.java create mode 100644 src/main/kotlin/xyz/deathsgun/modmanager/gui/ConfigScreen.kt diff --git a/src/main/java/xyz/deathsgun/modmanager/ModMenuEntrypoint.java b/src/main/java/xyz/deathsgun/modmanager/ModMenuEntrypoint.java new file mode 100644 index 0000000..9133635 --- /dev/null +++ b/src/main/java/xyz/deathsgun/modmanager/ModMenuEntrypoint.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; +import xyz.deathsgun.modmanager.gui.ConfigScreen; + +public class ModMenuEntrypoint implements ModMenuApi { + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return ConfigScreen::new; + } +} diff --git a/src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java b/src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java index 98afc41..e2e8c45 100644 --- a/src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java +++ b/src/main/java/xyz/deathsgun/modmanager/mixin/ModsScreenMixin.java @@ -1,3 +1,19 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package xyz.deathsgun.modmanager.mixin; import com.terraformersmc.modmenu.gui.ModsScreen; diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/ModInstallResult.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/ModInstallResult.kt index 582938a..00b3095 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/ModInstallResult.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/ModInstallResult.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package xyz.deathsgun.modmanager.api import net.minecraft.text.TranslatableText diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/ModRemoveResult.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/ModRemoveResult.kt index 23bdda9..6515c8a 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/ModRemoveResult.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/ModRemoveResult.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package xyz.deathsgun.modmanager.api import net.minecraft.text.TranslatableText diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/api/ModUpdateResult.kt b/src/main/kotlin/xyz/deathsgun/modmanager/api/ModUpdateResult.kt index 86fc279..2558317 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/api/ModUpdateResult.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/api/ModUpdateResult.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package xyz.deathsgun.modmanager.api import net.minecraft.text.TranslatableText diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt b/src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt index 5fcfaf2..80fe20c 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/config/Config.kt @@ -22,6 +22,8 @@ import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import net.fabricmc.loader.api.FabricLoader +import net.minecraft.text.Text +import net.minecraft.text.TranslatableText import xyz.deathsgun.modmanager.api.mod.VersionType import java.nio.charset.Charset import java.nio.file.Files @@ -46,7 +48,7 @@ data class Config( } @OptIn(ExperimentalSerializationApi::class) - private fun saveConfig(config: Config): Config { + fun saveConfig(config: Config): Config { try { val file = FabricLoader.getInstance().configDir.resolve("modmanager.json") val data = Json.encodeToString(config) @@ -58,7 +60,11 @@ data class Config( } enum class UpdateChannel { - ALL, STABLE, UNSTABLE + ALL, STABLE, UNSTABLE; + + fun text(): Text { + return TranslatableText(String.format("modmanager.channel.%s", name.lowercase())) + } } fun isReleaseAllowed(type: VersionType): Boolean { diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ConfigScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ConfigScreen.kt new file mode 100644 index 0000000..3d2574a --- /dev/null +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ConfigScreen.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.deathsgun.modmanager.gui + +import net.minecraft.client.font.MultilineText +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screen.ScreenTexts +import net.minecraft.client.gui.widget.ButtonWidget +import net.minecraft.client.gui.widget.CyclingButtonWidget +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.text.LiteralText +import net.minecraft.text.TranslatableText +import xyz.deathsgun.modmanager.ModManager +import xyz.deathsgun.modmanager.config.Config + +class ConfigScreen(private val previousScreen: Screen) : Screen(LiteralText("Config")) { + + private lateinit var defaultProvider: CyclingButtonWidget + private lateinit var updateChannel: CyclingButtonWidget + private var config: Config = ModManager.modManager.config.copy() + + override fun init() { + defaultProvider = addDrawableChild(CyclingButtonWidget.builder { LiteralText(it) } + .values(ModManager.modManager.provider.keys.toList()) + .initially(config.defaultProvider) + .build(width - 220, 30, 200, 20, TranslatableText("modmanager.button.defaultProvider")) + { _: CyclingButtonWidget, s: String -> config.defaultProvider = s }) + defaultProvider.active = ModManager.modManager.provider.size > 1 + + updateChannel = addDrawableChild(CyclingButtonWidget.builder { it.text() } + .values(Config.UpdateChannel.values().toList()) + .initially(config.updateChannel) + .build(width - 220, 60, 200, 20, TranslatableText("modmanager.button.updateChannel")) + { _: CyclingButtonWidget, channel: Config.UpdateChannel -> config.updateChannel = channel }) + + addDrawableChild(ButtonWidget( + width / 2 - 154, height - 28, 150, 20, ScreenTexts.CANCEL, + ) { + client!!.setScreen(previousScreen) + }) + addDrawableChild(ButtonWidget( + width / 2 + 4, height - 28, 150, 20, TranslatableText("modmanager.button.save") + ) { + ModManager.modManager.config = Config.saveConfig(this.config) + client!!.setScreen(previousScreen) + }) + + } + + override fun render(matrices: MatrixStack?, mouseX: Int, mouseY: Int, delta: Float) { + super.renderBackground(matrices) + + MultilineText.create(textRenderer, TranslatableText("modmanager.provider.info"), width - 230) + .draw(matrices, 10, 35, textRenderer.fontHeight, 0xFFFFFF) + MultilineText.create(textRenderer, TranslatableText("modmanager.channel.info"), width - 230) + .draw(matrices, 10, 65, textRenderer.fontHeight, 0xFFFFFF) + super.render(matrices, mouseX, mouseY, delta) + } + + override fun onClose() { + client!!.setScreen(previousScreen) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModProgressScreen.kt b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModProgressScreen.kt index 7d59f97..a239211 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModProgressScreen.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/gui/ModProgressScreen.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package xyz.deathsgun.modmanager.gui import kotlinx.coroutines.DelicateCoroutinesApi diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/state/ModState.kt b/src/main/kotlin/xyz/deathsgun/modmanager/state/ModState.kt index ed437c0..f3dd377 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/state/ModState.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/state/ModState.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package xyz.deathsgun.modmanager.state enum class ModState { diff --git a/src/main/kotlin/xyz/deathsgun/modmanager/state/SavedState.kt b/src/main/kotlin/xyz/deathsgun/modmanager/state/SavedState.kt index 7e26220..7ca43a3 100644 --- a/src/main/kotlin/xyz/deathsgun/modmanager/state/SavedState.kt +++ b/src/main/kotlin/xyz/deathsgun/modmanager/state/SavedState.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021 DeathsGun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package xyz.deathsgun.modmanager.state data class SavedState( diff --git a/src/main/resources/assets/modmanager/lang/en_us.json b/src/main/resources/assets/modmanager/lang/en_us.json index 82ca5dc..b6b422c 100644 --- a/src/main/resources/assets/modmanager/lang/en_us.json +++ b/src/main/resources/assets/modmanager/lang/en_us.json @@ -6,6 +6,9 @@ "modmanager.button.remove": "Remove", "modmanager.button.updateAll": "Update all", "modmanager.button.tryAgain": "Try again", + "modmanager.button.defaultProvider": "Default provider", + "modmanager.button.updateChannel": "Update channel", + "modmanager.button.save": "Save", "modmanager.categories": "Categories", "modmanager.category.updatable": "Updatable Mods", "modmanager.category.technology": "Technology", @@ -22,6 +25,10 @@ "modmanager.category.misc": "Miscellaneous", "modmanager.changes.title": "§e§lChanges detected§r", "modmanager.changes.message": "Changes will take effect after a relaunch.\nQuit Minecraft now?", + "modmanager.channel.info": "Switch between beta and stable versions", + "modmanager.channel.all": "All", + "modmanager.channel.stable": "Stable", + "modmanager.channel.unstable": "Unstable", "modmanager.details.author": "By %s", "modmanager.details.versioning": "From %s to %s", "modmanager.error.title": "§b§4Error:§r", @@ -44,6 +51,7 @@ "modmanager.search": "Search...", "modmanager.page.next": "Next Page", "modmanager.page.previous": "Previous Page", + "modmanager.provider.info": "Allows you to change the default provider for browsing", "modmanager.toast.update.title": "Updates Available!", "modmanager.toast.update.description": "%d mods can be updated" } \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 6655020..4a5293f 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -21,6 +21,9 @@ "adapter": "kotlin", "value": "xyz.deathsgun.modmanager.ModManager" } + ], + "modmenu": [ + "xyz.deathsgun.modmanager.ModMenuEntrypoint" ] }, "custom": { diff --git a/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt b/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt index 255610b..4539405 100644 --- a/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt +++ b/src/test/kotlin/xyz/deathsgun/modmanager/providers/modrinth/ModrinthTest.kt @@ -64,7 +64,7 @@ internal class ModrinthTest { @Test fun getModsByCategory() { - val result = modrinth.getMods(Category("misc", TranslatableText("")), 0, 10) + val result = modrinth.getMods(Category("misc", TranslatableText("")), Sorting.RELEVANCE, 0, 10) if (result is ModsResult.Error) { result.cause?.let { fail(result.text.key, it) @@ -77,7 +77,7 @@ internal class ModrinthTest { @Test fun getModsByQuery() { - val result = modrinth.search("Mod", 0, 10) + val result = modrinth.search("Mod", Sorting.DOWNLOADS, 0, 10) if (result is ModsResult.Error) { result.cause?.let { fail(result.text.key, it)