diff --git a/build.gradle.kts b/build.gradle.kts index 9be3e3c..3ac8928 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,92 +1,135 @@ plugins { - java - alias(libs.plugins.fabric.loom) - alias(libs.plugins.minotaur) -} + java -class ModInfo { - val id = property("mod.id").toString() - val group = property("mod.group").toString() - val version = property("mod.version").toString() + alias(libs.plugins.fabric.loom) + alias(libs.plugins.modpublish) } -val mod = ModInfo() +val mappingsAttribute = Attribute.of("net.minecraft.mappings", String::class.java)!! -version = "${mod.version}+${libs.versions.minecraft.get()}" -group = mod.group +val modVersion = "1.1.1" +val modId = "snapper" +val modName = "Snapper" -base.archivesName = mod.id +val modrinthProject = "snapper" +val githubRepository = "SpiritGameStudios/Snapper" -loom { - splitEnvironmentSourceSets() +group = "dev.spiritstudios" +base.archivesName = modId - mods.create(mod.id) { - sourceSet(sourceSets["main"]) - sourceSet(sourceSets["client"]) - } +version = "$modVersion+${libs.versions.minecraft.get()}" - accessWidenerPath = file("src/main/resources/snapper.accesswidener") +@Suppress("UnstableApiUsage") +repositories { + maven("https://maven.parchmentmc.org/") { + name = "ParchmentMC" + content { includeGroupAndSubgroups("org.parchmentmc") } + } + + maven("https://maven.terraformersmc.com/") { + name = "Terraformers" + content { includeGroupAndSubgroups("com.terraformersmc") } + } + + maven("https://moehreag.duckdns.org/maven/releases/") { + name = "AxolotlClient Releases" + content { includeGroupAndSubgroups("io.github.axolotlclient") } + } + + maven("https://maven.greenhouse.lgbt/releases/") { + name = "Greenhouse Releases" + content { includeGroupAndSubgroups("lgbt.greenhouse") } + } + + maven("https://maven.greenhouse.lgbt/snapshots/") { + name = "Greenhouse Snapshots" + content { includeGroupAndSubgroups("lgbt.greenhouse") } + } + + mavenCentral() } -repositories { - mavenCentral() - maven("https://maven.spiritstudios.dev/releases/") - maven("https://moehreag.duckdns.org/maven/releases") { - content { - includeGroup("io.github.axolotlclient.AxolotlClient") - includeGroup("io.github.axolotlclient.AxolotlClient-config") - } - } +loom { + runtimeOnlyLog4j = true + + splitEnvironmentSourceSets() + + mods.create(modId) { + sourceSet(sourceSets["main"]) + sourceSet(sourceSets["client"]) + } + + accessWidenerPath = file("src/main/resources/snapper.classtweaker") } dependencies { - minecraft(libs.minecraft) - mappings(variantOf(libs.yarn) { classifier("v2") }) - modImplementation(libs.fabric.loader) + minecraft(libs.minecraft) + @Suppress("UnstableApiUsage") + mappings( + loom.layered { + officialMojangMappings() + parchment(libs.parchment) + } + ) + + modImplementation(libs.fabric.loader) + modImplementation(libs.fabric.api) + + modCompileOnlyApi(libs.greenhouse.config.api) { + attributes { attribute(mappingsAttribute, "intermediary") } + } - modImplementation(libs.fabric.api) + modRuntimeOnly(libs.greenhouse.config) + include(libs.greenhouse.config) - include(libs.bundles.specter) - modImplementation(libs.bundles.specter) + modCompileOnly(libs.modmenu) - implementation(libs.objc.bridge) + implementation(libs.objc.bridge) } tasks.processResources { - val map = mapOf( - "mod_id" to mod.id, - "mod_version" to mod.version, - "fabric_loader_version" to libs.versions.fabric.loader.get(), - "minecraft_version" to libs.versions.minecraft.get() - ) - - inputs.properties(map) - filesMatching("fabric.mod.json") { expand(map) } + val map = mapOf( + "version" to modVersion, + "loader_version" to libs.versions.fabric.loader.get() + ) + + inputs.properties(map) + + filesMatching("fabric.mod.json") { expand(map) } } java { - withSourcesJar() + withSourcesJar() - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } tasks.withType { - options.encoding = "UTF-8" - options.release = 21 + options.encoding = "UTF-8" + options.release = 21 } -tasks.jar { from("LICENSE") { rename { "${it}_${base.archivesName.get()}" } } } - -modrinth { - token.set(System.getenv("MODRINTH_TOKEN")) - projectId.set(mod.id) - versionNumber.set(mod.version) - uploadFile.set(tasks.remapJar) - gameVersions.addAll(libs.versions.minecraft.get(), "1.21.8") - loaders.addAll("fabric", "quilt") - syncBodyFrom.set(rootProject.file("README.md").readText()) - dependencies { - required.version("fabric-api", libs.versions.fabric.api.get()) - } -} \ No newline at end of file +tasks.jar { + from("LICENSE") { rename { "${it}_$modId" } } +} + +publishMods { + file = tasks.remapJar.get().archiveFile + modLoaders.add("fabric") + + version = modVersion + type = STABLE + displayName = "$modName $modVersion for Minecraft ${libs.versions.minecraft.get()}" + + modrinth { + accessToken = providers.gradleProperty("secrets.modrinth_token") + projectId = modrinthProject + minecraftVersions.add(libs.versions.minecraft.get()) + + projectDescription = providers.fileContents(layout.projectDirectory.file("README.md")).asText + + requires("fabric-api") + embeds("greenhouse-config") + } +} diff --git a/gradle.properties b/gradle.properties index 96f7b12..88c87ca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ -org.gradle.jvmargs=-Xmx2G +org.gradle.jvmargs=-Xmx1G org.gradle.parallel=true -mod.version = 1.1 -mod.group = dev.spiritstudios -mod.id = snapper +# IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349 +# If you really want to use this, it may work on some tasks if you set "Shorten command line" in your InteliJ run configuration to "none". +org.gradle.configuration-cache=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f42ed3..769d1de 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,38 +1,32 @@ [versions] -fabric_loom = "1.11-SNAPSHOT" -minotaur = "2.+" +fabric_loom = "1.15-SNAPSHOT" +modpublish = "1.1.0" -minecraft = "1.21.8" -yarn = "1.21.8+build.1" +minecraft = "1.21.10" +parchment = "2025.10.12" -fabric_loader = "0.17.2" -fabric_api = "0.133.4+1.21.8" +fabric_loader = "0.18.1" +fabric_api = "0.138.3+1.21.10" + +greenhouse_config = "3.0.0-beta.4+1.21.10" +modmenu = "16.0.0" -specter = "1.3.0" objc_bridge = "1.0.0" [plugins] -fabric_loom = { id = "fabric-loom", version.ref = "fabric_loom" } -minotaur = { id = "com.modrinth.minotaur", version.ref = "minotaur" } +fabric_loom = { id = "net.fabricmc.fabric-loom-remap", version.ref = "fabric_loom" } +modpublish = { id = "me.modmuss50.mod-publish-plugin", version.ref = "modpublish" } [libraries] minecraft = { group = "mojang", name = "minecraft", version.ref = "minecraft" } -yarn = { group = "net.fabricmc", name = "yarn", version.ref = "yarn" } +parchment = { group = "org.parchmentmc.data", name = "parchment-1.21.10", version.ref = "parchment" } fabric_loader = { group = "net.fabricmc", name = "fabric-loader", version.ref = "fabric_loader" } fabric_api = { group = "net.fabricmc.fabric-api", name = "fabric-api", version.ref = "fabric_api" } -specter_config = { group = "dev.spiritstudios.specter", name = "config", version.ref = "specter" } -specter_core = { group = "dev.spiritstudios.specter", name = "core", version.ref = "specter" } -specter_serialization = { group = "dev.spiritstudios.specter", name = "serialization", version.ref = "specter" } -specter_gui = { group = "dev.spiritstudios.specter", name = "gui", version.ref = "specter" } +greenhouse_config_api = { group = "lgbt.greenhouse.config", name = "greenhouse-config-api", version.ref = "greenhouse_config" } +greenhouse_config = { group = "lgbt.greenhouse.config", name = "greenhouse-config-fabric", version.ref = "greenhouse_config" } -objc_bridge = { group = "ca.weblite", name = "java-objc-bridge", version.ref = "objc_bridge" } +modmenu = { group = "com.terraformersmc", name = "modmenu", version.ref = "modmenu" } -[bundles] -specter = [ - "specter_serialization", - "specter_core", - "specter_config", - "specter_gui" -] \ No newline at end of file +objc_bridge = { group = "ca.weblite", name = "java-objc-bridge", version.ref = "objc_bridge" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ca025c8..23449a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/client/java/dev/spiritstudios/snapper/Snapper.java b/src/client/java/dev/spiritstudios/snapper/Snapper.java index 6175f77..978dd6d 100644 --- a/src/client/java/dev/spiritstudios/snapper/Snapper.java +++ b/src/client/java/dev/spiritstudios/snapper/Snapper.java @@ -1,40 +1,24 @@ package dev.spiritstudios.snapper; -import dev.spiritstudios.snapper.util.PlatformHelper; -import dev.spiritstudios.snapper.util.actions.GeneralPlatformActions; -import dev.spiritstudios.snapper.util.actions.MacPlatformActions; -import dev.spiritstudios.snapper.util.config.DirectoryConfigUtil; import dev.spiritstudios.snapper.util.uploading.ScreenshotUploading; -import dev.spiritstudios.specter.api.config.client.ConfigScreenWidgets; -import dev.spiritstudios.specter.api.config.client.ModMenuHelper; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; -import net.minecraft.client.MinecraftClient; -import net.minecraft.util.Identifier; +import net.minecraft.resources.ResourceLocation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.nio.file.Path; - public final class Snapper implements ClientModInitializer { - public static final String MODID = "snapper"; - public static final Logger LOGGER = LoggerFactory.getLogger(MODID); + public static final String MOD_ID = "snapper"; + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); @Override public void onInitializeClient() { - ConfigScreenWidgets.add(Path.class, DirectoryConfigUtil.PATH_WIDGET_FACTORY); + SnapperConfig.init(); SnapperKeybindings.init(); - - ModMenuHelper.addConfig(Snapper.MODID, SnapperConfig.HOLDER.id()); - ClientLifecycleEvents.CLIENT_STOPPING.register(client -> ScreenshotUploading.close()); } - public static Identifier id(String path) { - return Identifier.of(MODID, path); - } - - public static PlatformHelper getPlatformHelper() { - return MinecraftClient.IS_SYSTEM_MAC ? new MacPlatformActions() : new GeneralPlatformActions(); + public static ResourceLocation id(String path) { + return ResourceLocation.fromNamespaceAndPath(MOD_ID, path); } } \ No newline at end of file diff --git a/src/client/java/dev/spiritstudios/snapper/SnapperConfig.java b/src/client/java/dev/spiritstudios/snapper/SnapperConfig.java index 9964342..946d47e 100644 --- a/src/client/java/dev/spiritstudios/snapper/SnapperConfig.java +++ b/src/client/java/dev/spiritstudios/snapper/SnapperConfig.java @@ -1,51 +1,249 @@ package dev.spiritstudios.snapper; -import dev.spiritstudios.snapper.gui.screen.ScreenshotScreen; +import com.mojang.serialization.Codec; +import dev.spiritstudios.snapper.gui.screen.ScreenshotListScreen; import dev.spiritstudios.snapper.util.SnapperUtil; -import dev.spiritstudios.snapper.util.config.DirectoryConfigUtil; +import dev.spiritstudios.snapper.util.DirectoryConfigUtil; import dev.spiritstudios.snapper.util.uploading.AxolotlClientApi; -import dev.spiritstudios.specter.api.config.Config; -import dev.spiritstudios.specter.api.config.ConfigHolder; -import dev.spiritstudios.specter.api.config.Value; +import lgbt.greenhouse.config.api.v3.GreenhouseConfigSide; +import lgbt.greenhouse.config.api.v3.GreenhouseConfigHolder; +import lgbt.greenhouse.config.api.v3.dfu.builder.DataFixerBuilderFunctions; +import lgbt.greenhouse.config.api.v3.dfu.builder.schema.TypeTemplateBuilder; +import lgbt.greenhouse.config.api.v3.dfu.fix.GreenhouseConfigRelocateFieldsFix; +import lgbt.greenhouse.config.api.v3.lang.GreenhouseConfigJsonCLang; +import lgbt.greenhouse.config.api.v3.lang.GreenhouseConfigJsonLang; +import net.minecraft.Util; import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; -public final class SnapperConfig extends Config { - public static final ConfigHolder HOLDER = ConfigHolder.builder( - Snapper.id("snapper"), SnapperConfig.class - ).build(); +public record SnapperConfig(boolean copyTakenScreenshot, + SnapperButton snapperButton, + ScreenshotListScreen.ViewMode viewMode, + SnapperUtil.PanoramaSize panoramaDimensions, + CustomScreenshotFolder customScreenshotPath, + AxolotlClient axolotlClient) { + public static final GreenhouseConfigHolder HOLDER = GreenhouseConfigHolder.register( + SnapperConfig.class, + Snapper.MOD_ID, + 10101, + GreenhouseConfigJsonCLang.INSTANCE, + GreenhouseConfigSide.CLIENT, + configBuilder -> configBuilder + .withValue( + "copy_taken_screenshot", + "Whether to copy screenshots to the clipboard when taken", + Codec.BOOL, + false, + SnapperConfig::copyTakenScreenshot + ).withMapValue( + SnapperButton.class, + "snapper_button", + "Settings relating to the Snapper Button", + SnapperConfig::snapperButton, + mapBuilder -> mapBuilder + .withValue( + "show_on_title_screen", + "Whether to show Snapper button on title screen", + Codec.BOOL, + true, + SnapperButton::showOnTitleScreen + ).withValue( + "show_in_game_menu", + "Whether to show Snapper button in game menu", + Codec.BOOL, + true, + SnapperButton::showInGameMenu + ) + ).withValue( + "view_mode", + """ + Whether to show the screenshot menu in a grid or a list + May be either 'grid' or 'list'""", + ScreenshotListScreen.ViewMode.CODEC, + ScreenshotListScreen.ViewMode.GRID, + SnapperConfig::viewMode + ).withValue( + "panorama_size", + """ + Dimensions of individual panorama images when saved + May be 1024, 2048, or 4096""", + SnapperUtil.PanoramaSize.CODEC, + SnapperUtil.PanoramaSize.ONE_THOUSAND_TWENTY_FOUR, + SnapperConfig::panoramaDimensions + ).withMapValue( + CustomScreenshotFolder.class, + "custom_screenshot_path", + "Settings relating to a custom screenshot path", + SnapperConfig::customScreenshotPath, + mapBuilder -> mapBuilder + .withValue( + "enabled", + "Whether to use a custom screenshot path instead of Minecraft's default", + Codec.BOOL, + false, + CustomScreenshotFolder::enabled + ).withValue( + "path", + "The path to use if custom screenshot folders are enabled", + DirectoryConfigUtil.PATH_CODEC, + SnapperUtil.UNIFIED_FOLDER, + CustomScreenshotFolder::path + ) + ).withMapValue( + AxolotlClient.class, + "axolotl_client", + "Settings relating to Axolotl Client", + SnapperConfig::axolotlClient, + mapBuilder -> mapBuilder + .withValue( + "terms_status", + """ + Whether the terms of AxolotlClient have been accepted + These terms must be accepted to share screenshots via AxolotlClient's image host + May be 'accept', 'deny', or 'unset'""", + AxolotlClientApi.TermsAcceptance.CODEC, + AxolotlClientApi.TermsAcceptance.UNSET, + AxolotlClient::termsStatus + ) + ), + dataFixerBuilder -> dataFixerBuilder + .withSchema( + 0, + schemaBuilder -> + schemaBuilder + .withField("copyTakenScreenshot", TypeTemplateBuilder.BOOL) + .withField("showSnapperTitleScreen", TypeTemplateBuilder.BOOL) + .withField("showSnapperGameMenu", TypeTemplateBuilder.BOOL) + .withField("viewMode", TypeTemplateBuilder.STRING) + .withField("termsAccepted", TypeTemplateBuilder.STRING) + .withField("panoramaDimensions", TypeTemplateBuilder.STRING) + .withField("useCustomScreenshotFolder", TypeTemplateBuilder.BOOL) + .withField("customScreenshotFolder", TypeTemplateBuilder.STRING) + ).withPreviousLang(0, GreenhouseConfigJsonLang.INSTANCE) + .withSchemaAndFixes( + 10100, + DataFixerBuilderFunctions.create( + builder -> builder + .withField("copy_taken_screenshot", TypeTemplateBuilder.BOOL) + .withField("snapper_button", TypeTemplateBuilder.map( + mapBuilder -> mapBuilder + .withField("show_on_title_screen", TypeTemplateBuilder.BOOL) + .withField("show_in_game_menu", TypeTemplateBuilder.BOOL) + )) + .withField("view_mode", TypeTemplateBuilder.STRING) + .withField("panorama_dimensions", TypeTemplateBuilder.STRING) + .withField("custom_screenshot_path", TypeTemplateBuilder.map( + mapBuilder -> mapBuilder + .withField("enabled", TypeTemplateBuilder.BOOL) + .withField("path", TypeTemplateBuilder.STRING) + )) + .withField("axolotl_client", TypeTemplateBuilder.map( + mapBuilder -> mapBuilder + .withField("terms_status", TypeTemplateBuilder.STRING) + )) + .nonRecursive(), + schema -> GreenhouseConfigRelocateFieldsFix.create( + schema, + GreenhouseConfigRelocateFieldsFix.data("copyTakenScreenshot", "copy_taken_screenshot"), + GreenhouseConfigRelocateFieldsFix.data("showSnapperTitleScreen", "snapper_button.show_on_title_screen"), + GreenhouseConfigRelocateFieldsFix.data("showSnapperGameMenu", "snapper_button.show_in_game_menu"), + GreenhouseConfigRelocateFieldsFix.data("viewMode", "view_mode"), + GreenhouseConfigRelocateFieldsFix.data("panoramaDimensions", "panorama_dimensions"), + GreenhouseConfigRelocateFieldsFix.data("useCustomScreenshotFolder", "custom_screenshot_path.enabled"), + GreenhouseConfigRelocateFieldsFix.data("customScreenshotFolder", "custom_screenshot_path.path"), + GreenhouseConfigRelocateFieldsFix.data("termsAccepted", "axolotl_client.terms_status") + ) + ) + ) + ); - public static final SnapperConfig INSTANCE = HOLDER.get(); + public record SnapperButton(boolean showOnTitleScreen, boolean showInGameMenu) { + } - public final Value copyTakenScreenshot = booleanValue(false) - .comment("Whether to copy screenshots to clipboard when taken.") - .build(); + public record CustomScreenshotFolder(boolean enabled, Path path) { + } - public final Value showSnapperTitleScreen = booleanValue(true) - .comment("Whether to show Snapper button on title screen.") - .build(); + public record AxolotlClient(AxolotlClientApi.TermsAcceptance termsStatus) { + } - public final Value showSnapperGameMenu = booleanValue(true) - .comment("Whether to show Snapper button in game menu.") - .build(); + public static void init() { + } - public final Value viewMode = enumValue(ScreenshotScreen.ViewMode.GRID, ScreenshotScreen.ViewMode.class) - .comment("Whether to show screenshot menu with grid or list.") - .build(); + public static Mutable mutable() { + return new Mutable(); + } - public final Value termsAccepted = enumValue(AxolotlClientApi.TermsAcceptance.UNSET, AxolotlClientApi.TermsAcceptance.class) - .comment("Whether the terms of AxolotlClient have been accepted.") - .build(); + public static void edit(Consumer editor) { + var mutable = mutable(); + editor.accept(mutable); + mutable.save(); + } - public final Value panoramaDimensions = enumValue(SnapperUtil.PanoramaSize.ONE_THOUSAND_TWENTY_FOUR, SnapperUtil.PanoramaSize.class) - .comment("Dimensions of individual panorama images when saved.") - .build(); + public static CompletableFuture editAsync(Consumer editor) { + var mutable = mutable(); + editor.accept(mutable); + return mutable.saveAsync(); + } - public final Value useCustomScreenshotFolder = booleanValue(false) - .comment("Whether to use a custom screenshot folder instead of Minecraft's default") - .build(); + public static class Mutable { + // Root + public boolean copyTakenScreenshot; + public ScreenshotListScreen.ViewMode viewMode; + public SnapperUtil.PanoramaSize panoramaDimensions; - public final Value customScreenshotFolder = value(SnapperUtil.UNIFIED_FOLDER, DirectoryConfigUtil.PATH_CODEC) - .comment("What folder to use if custom screenshot folders are enabled.") - .build(); + // Snapper Button + public boolean showOnTitleScreen; + public boolean showInGameMenu; + + // Custom Screenshot Folder + public boolean enabled; + public Path path; + + // Axolotl Client + public AxolotlClientApi.TermsAcceptance termsAccepted; + + public Mutable() { + SnapperConfig config = HOLDER.get(); + copyTakenScreenshot = config.copyTakenScreenshot; + viewMode = config.viewMode; + panoramaDimensions = config.panoramaDimensions; + + showOnTitleScreen = config.snapperButton.showOnTitleScreen; + showInGameMenu = config.snapperButton.showInGameMenu; + + enabled = config.customScreenshotPath.enabled; + path = config.customScreenshotPath.path; + + termsAccepted = config.axolotlClient.termsStatus; + } + + public void save() { + HOLDER.save(build(), null); + } + + public CompletableFuture saveAsync() { + return CompletableFuture.runAsync(this::save, Util.ioPool()); + } + + private SnapperConfig build() { + return new SnapperConfig( + copyTakenScreenshot, + new SnapperButton( + showOnTitleScreen, + showInGameMenu + ), + viewMode, + panoramaDimensions, + new CustomScreenshotFolder( + enabled, + path + ), + new AxolotlClient( + termsAccepted + ) + ); + } + } } diff --git a/src/client/java/dev/spiritstudios/snapper/SnapperKeybindings.java b/src/client/java/dev/spiritstudios/snapper/SnapperKeybindings.java index 6008ffb..b89a644 100644 --- a/src/client/java/dev/spiritstudios/snapper/SnapperKeybindings.java +++ b/src/client/java/dev/spiritstudios/snapper/SnapperKeybindings.java @@ -1,38 +1,39 @@ package dev.spiritstudios.snapper; -import dev.spiritstudios.snapper.gui.screen.ScreenshotScreen; +import dev.spiritstudios.snapper.gui.screen.ScreenshotListScreen; import dev.spiritstudios.snapper.gui.screen.ScreenshotViewerScreen; import dev.spiritstudios.snapper.gui.toast.SnapperToast; -import dev.spiritstudios.snapper.util.DynamicTexture; +import dev.spiritstudios.snapper.util.ScreenshotTexture; import dev.spiritstudios.snapper.util.ScreenshotActions; -import dev.spiritstudios.snapper.util.SnapperUtil; -import dev.spiritstudios.specter.api.core.client.event.ClientKeybindEvents; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.option.KeyBinding; -import net.minecraft.text.Text; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; import org.lwjgl.glfw.GLFW; import java.nio.file.Path; import java.util.List; public final class SnapperKeybindings { - public static final KeyBinding PANORAMA_KEY = new KeyBinding( + public static final KeyMapping.Category SNAPPER = KeyMapping.Category.register(Snapper.id(Snapper.MOD_ID)); + + public static final KeyMapping PANORAMA_KEY = new KeyMapping( "key.snapper.panorama", GLFW.GLFW_KEY_F8, - "key.categories.snapper" + SNAPPER ); - public static final KeyBinding RECENT_SCREENSHOT_KEY = new KeyBinding( + public static final KeyMapping RECENT_SCREENSHOT_KEY = new KeyMapping( "key.snapper.recent", GLFW.GLFW_KEY_O, - "key.categories.snapper" + SNAPPER ); - public static final KeyBinding SCREENSHOT_MENU_KEY = new KeyBinding( + public static final KeyMapping SCREENSHOT_MENU_KEY = new KeyMapping( "key.snapper.screenshot_menu", GLFW.GLFW_KEY_V, - "key.categories.snapper" + SNAPPER ); public static void init() { @@ -40,53 +41,53 @@ public static void init() { KeyBindingHelper.registerKeyBinding(RECENT_SCREENSHOT_KEY); KeyBindingHelper.registerKeyBinding(SCREENSHOT_MENU_KEY); - ClientKeybindEvents.pressed(SCREENSHOT_MENU_KEY).register(client -> - client.setScreen(new ScreenshotScreen(client.currentScreen))); - - ClientKeybindEvents.pressed(PANORAMA_KEY).register(SnapperKeybindings::takePanorama); - ClientKeybindEvents.pressed(RECENT_SCREENSHOT_KEY).register(SnapperKeybindings::openRecentScreenshot); + ClientTickEvents.END_CLIENT_TICK.register(client -> { + while (PANORAMA_KEY.consumeClick()) SnapperKeybindings.takePanorama(client); + while (RECENT_SCREENSHOT_KEY.consumeClick()) SnapperKeybindings.openRecentScreenshot(client); + while (SCREENSHOT_MENU_KEY.consumeClick()) client.setScreen(new ScreenshotListScreen(client.screen)); + }); } - private static void takePanorama(MinecraftClient client) { + private static void takePanorama(Minecraft client) { if (client.player == null) return; - client.takePanorama(client.runDirectory); + client.grabPanoramixScreenshot(client.gameDirectory); - SnapperUtil.toast( + SnapperToast.push( SnapperToast.Type.PANORAMA, - Text.translatable("toast.snapper.panorama.created"), - Text.translatable( + Component.translatable("toast.snapper.panorama.created"), + Component.translatable( "toast.snapper.panorama.created.description", - SCREENSHOT_MENU_KEY.getBoundKeyLocalizedText() + SCREENSHOT_MENU_KEY.getTranslatedKeyMessage() ) ); } - private static void openRecentScreenshot(MinecraftClient client) { + private static void openRecentScreenshot(Minecraft client) { List screenshots = ScreenshotActions.getScreenshots(); if (screenshots.isEmpty()) { - SnapperUtil.toast( + SnapperToast.push( SnapperToast.Type.SCREENSHOT, - Text.translatable("toast.snapper.screenshot.recent.failure"), - Text.translatable("toast.snapper.screenshot.recent.failure.not_exist") + Component.translatable("toast.snapper.screenshot.recent.failure"), + Component.translatable("toast.snapper.screenshot.recent.failure.not_exist") ); return; } Path latestPath = screenshots.getFirst(); - DynamicTexture.createScreenshot(client.getTextureManager(), latestPath) + ScreenshotTexture.createScreenshot(client.getTextureManager(), latestPath) .ifPresentOrElse( image -> { client.setScreen(new ScreenshotViewerScreen( image, latestPath, - client.currentScreen + client.screen )); image.load(); }, - () -> SnapperUtil.toast( + () -> SnapperToast.push( SnapperToast.Type.DENY, - Text.translatable("toast.snapper.screenshot.recent.failure"), - Text.translatable("toast.snapper.screenshot.recent.failure.generic") + Component.translatable("toast.snapper.screenshot.recent.failure"), + Component.translatable("toast.snapper.screenshot.recent.failure.generic") ) ); diff --git a/src/client/java/dev/spiritstudios/snapper/compat/SnapperModMenu.java b/src/client/java/dev/spiritstudios/snapper/compat/SnapperModMenu.java new file mode 100644 index 0000000..06357c8 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/compat/SnapperModMenu.java @@ -0,0 +1,12 @@ +package dev.spiritstudios.snapper.compat; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; +import dev.spiritstudios.snapper.gui.screen.ConfigScreen; + +public final class SnapperModMenu implements ModMenuApi { + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return ConfigScreen::new; + } +} diff --git a/src/client/java/dev/spiritstudios/snapper/gui/overlay/ExternalDialogOverlay.java b/src/client/java/dev/spiritstudios/snapper/gui/overlay/ExternalDialogOverlay.java index 404f4a9..91c0901 100644 --- a/src/client/java/dev/spiritstudios/snapper/gui/overlay/ExternalDialogOverlay.java +++ b/src/client/java/dev/spiritstudios/snapper/gui/overlay/ExternalDialogOverlay.java @@ -1,51 +1,45 @@ package dev.spiritstudios.snapper.gui.overlay; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gl.RenderPipelines; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.screen.Overlay; -import net.minecraft.client.util.InputUtil; -import net.minecraft.text.Text; -import net.minecraft.util.Colors; -import net.minecraft.util.Identifier; +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Overlay; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.CommonColors; public class ExternalDialogOverlay extends Overlay { - private final MinecraftClient client = MinecraftClient.getInstance(); + private final Minecraft client = Minecraft.getInstance(); - public static final Identifier MENU_BACKGROUND_TEXTURE = Identifier.ofVanilla("textures/gui/menu_background.png"); - private static final Identifier INWORLD_MENU_BACKGROUND_TEXTURE = Identifier.ofVanilla("textures/gui/inworld_menu_background.png"); + public static final ResourceLocation MENU_BACKGROUND_TEXTURE = ResourceLocation.withDefaultNamespace("textures/gui/menu_background.png"); + private static final ResourceLocation INWORLD_MENU_BACKGROUND_TEXTURE = ResourceLocation.withDefaultNamespace("textures/gui/inworld_menu_background.png"); @Override - public boolean pausesGame() { - return false; - } - - @Override - public void render(DrawContext context, int mouseX, int mouseY, float delta) { - if (this.client.currentScreen != null) { - this.client.currentScreen.renderBackground(context, mouseX, mouseY, delta); - this.client.currentScreen.render(context, mouseX, mouseY, delta); + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + if (this.client.screen != null) { + this.client.screen.renderWithTooltipAndSubtitles(graphics, mouseX, mouseY, partialTick); } - this.client.gameRenderer.renderBlur(); + graphics.nextStratum(); - context.drawTexture( + graphics.blit( RenderPipelines.GUI_TEXTURED, - this.client.world == null ? MENU_BACKGROUND_TEXTURE : INWORLD_MENU_BACKGROUND_TEXTURE, + this.client.level == null ? MENU_BACKGROUND_TEXTURE : INWORLD_MENU_BACKGROUND_TEXTURE, 0, 0, 0, 0, - context.getScaledWindowWidth(), context.getScaledWindowHeight(), + graphics.guiWidth(), graphics.guiHeight(), 32, 32 ); - context.drawCenteredTextWithShadow( - client.textRenderer, - Text.translatable("overlay.snapper.external_dialog.folder"), - context.getScaledWindowWidth() / 2, context.getScaledWindowHeight() / 2, - Colors.WHITE + graphics.drawCenteredString( + client.font, + Component.translatable("overlay.snapper.external_dialog.folder"), + graphics.guiWidth() / 2, graphics.guiHeight() / 2, + CommonColors.WHITE ); - if (InputUtil.isKeyPressed(client.getWindow().getHandle(), InputUtil.GLFW_KEY_ESCAPE)) close(); + if (InputConstants.isKeyDown(client.getWindow(), InputConstants.KEY_ESCAPE)) close(); } public void close() { diff --git a/src/client/java/dev/spiritstudios/snapper/gui/screen/ConfigScreen.java b/src/client/java/dev/spiritstudios/snapper/gui/screen/ConfigScreen.java new file mode 100644 index 0000000..08d8c1e --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/gui/screen/ConfigScreen.java @@ -0,0 +1,261 @@ +package dev.spiritstudios.snapper.gui.screen; + +import dev.spiritstudios.snapper.SnapperConfig; +import dev.spiritstudios.snapper.gui.widget.ConfigList; +import dev.spiritstudios.snapper.gui.widget.ConfigSliderWidget; +import dev.spiritstudios.snapper.gui.widget.FolderSelectWidget; +import dev.spiritstudios.snapper.util.SnapperUtil; +import dev.spiritstudios.snapper.util.uploading.AxolotlClientApi; +import net.minecraft.client.gui.components.*; +import net.minecraft.client.gui.layouts.HeaderAndFooterLayout; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +public class ConfigScreen extends Screen { + private final @Nullable Screen lastScreen; + + public final HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this); + private ConfigList list; + public final SnapperConfig.Mutable config = new SnapperConfig.Mutable(); + + public ConfigScreen(@Nullable Screen lastScreen) { + super(Component.translatable("config.snapper.title")); + this.lastScreen = lastScreen; + } + + @Override + protected void init() { + super.init(); + + this.layout.addTitleHeader(this.title, this.font); + + this.list = this.layout.addToContents(new ConfigList(this.minecraft, this.width, this)); + + this.list.addHeader(Component.translatable("config.snapper.general")); + + this.list.addSmall( + booleanButton( + "copyTakenScreenshot", + b -> config.copyTakenScreenshot = b, + config.copyTakenScreenshot + ), + enumButton( + "viewMode", + b -> config.viewMode = b, + config.viewMode, + ScreenshotListScreen.ViewMode.class + ) + ); + + this.list.addBig( + enumSlider( + "panoramaDimensions", + b -> config.panoramaDimensions = b, + config.panoramaDimensions, + SnapperUtil.PanoramaSize.class + ) + ); + + this.list.addHeader(Component.translatable("config.snapper.snapperButton")); + + this.list.addSmall( + booleanButton( + "showOnTitleScreen", + b -> config.showOnTitleScreen = b, + config.showOnTitleScreen + ), + booleanButton( + "showInGameMenu", + b -> config.showInGameMenu = b, + config.showInGameMenu + ) + ); + + this.list.addHeader(Component.translatable("config.snapper.customScreenshotFolder")); + + var folderSelect = folderSelectWidget( + "customScreenshotFolder", + b -> config.path = b, + config.path + ); + + folderSelect.setActive(config.enabled); + + this.list.addBig(booleanButton( + "customScreenshotFolderEnabled", + b -> { + folderSelect.setActive(b); + config.enabled = b; + }, + config.enabled + )); + + this.list.addBig(folderSelect); + + this.list.addHeader(Component.translatable("config.snapper.uploading")); + + this.list.addBig(enumButton( + "termsAccepted", + b -> config.termsAccepted = b, + config.termsAccepted, + AxolotlClientApi.TermsAcceptance.class + )); + + this.layout.addToFooter( + Button.builder(CommonComponents.GUI_DONE, button -> this.onClose()) + .width(200) + .build() + ); + + this.layout.visitWidgets(this::addRenderableWidget); + this.repositionElements(); + } + + @Override + protected void repositionElements() { + this.layout.arrangeElements(); + if (this.list != null) { + this.list.updateSize(this.width, this.layout); + } + } + + private @Nullable Tooltip getTooltip(String name) { + Component tooltipText = Component.translatableWithFallback("config.snapper." + name + ".tooltip", ""); + return tooltipText.getString().isEmpty() ? + null : + Tooltip.create(tooltipText); + } + + private AbstractWidget booleanButton(String name, Consumer setter, boolean currentValue) { + Tooltip tooltip = getTooltip(name); + + return CycleButton.builder( + boolean_ -> boolean_ + ? CommonComponents.OPTION_ON + : CommonComponents.OPTION_OFF + ) + .withValues(List.of(Boolean.TRUE, Boolean.FALSE)) + .withTooltip(b -> tooltip) + .withInitialValue(currentValue) + .create( + 0, 0, + 150, 20, + Component.translatable("config.snapper." + name), + (cycleButton, object) -> { + setter.accept(object); + } + ); + } + + private > AbstractWidget enumButton( + String name, + Consumer setter, + T currentValue, + Class clazz + ) { + Tooltip tooltip = getTooltip(name); + + return CycleButton.builder( + t -> Component.translatable("config.snapper." + name + "." + t.toString().toLowerCase()) + ) + .withValues(Arrays.asList(clazz.getEnumConstants())) + .withTooltip(b -> tooltip) + .withInitialValue(currentValue) + .create( + 0, 0, + 150, 20, + Component.translatable("config.snapper." + name), + (cycleButton, object) -> { + setter.accept(object); + } + ); + } + + private > AbstractWidget enumSlider( + String name, + Consumer setter, + T currentValue, + Class clazz + ) { + Tooltip tooltip = getTooltip(name); + List values = Arrays.asList(clazz.getEnumConstants()); + + return new ConfigSliderWidget<>( + 0, 0, + 150, 20, + Component.translatable("config.snapper." + name), + currentValue, + slider -> { + if (slider >= 1.0) { + slider = 0.99999F; + } + + int index = Mth.floor(Mth.map(slider, 0.0, 1.0, 0.0, values.size())); + return values.get(Mth.clamp(index, 0, values.size() - 1)); + }, + value -> { + if (value == values.getFirst()) { + return 0.0; + } else { + return value == values.getLast() ? 1.0 : Mth.map(values.indexOf(value), 0.0, values.size() - 1, 0.0, 1.0); + } + }, + t -> Component.translatable("config.snapper." + name + "." + t.toString().toLowerCase()), + t -> tooltip, + setter + ); + } + + private FolderSelectWidget folderSelectWidget(String name, Consumer setter, Path currentValue) { + Tooltip tooltip = getTooltip(name); + + FolderSelectWidget widget = new FolderSelectWidget( + 0, 0, + 150, 20, + new FolderSelectWidget.PathFunctions() { + private Path value = currentValue; + + @Override + public Path get() { + return value; + } + + @Override + public void set(Path path) { + value = path; + setter.accept(path); + } + + @Override + public void reset() { + value = currentValue; + } + }, + "config.snapper." + name + ".placeholder" + ); + + widget.setTooltip(tooltip); + + return widget; + } + + @Override + public void onClose() { + assert minecraft != null; + + minecraft.setScreen(lastScreen); + config.saveAsync().thenRun(() -> { + if (lastScreen instanceof ScreenshotListScreen screenshotsScreen) { + screenshotsScreen.refresh(); + } + }); + } +} diff --git a/src/client/java/dev/spiritstudios/snapper/gui/screen/PanoramaViewerScreen.java b/src/client/java/dev/spiritstudios/snapper/gui/screen/PanoramaViewerScreen.java index d45ecb9..4b0fd60 100644 --- a/src/client/java/dev/spiritstudios/snapper/gui/screen/PanoramaViewerScreen.java +++ b/src/client/java/dev/spiritstudios/snapper/gui/screen/PanoramaViewerScreen.java @@ -4,17 +4,17 @@ import dev.spiritstudios.snapper.util.DynamicCubemapTexture; import dev.spiritstudios.snapper.util.SafeFiles; import dev.spiritstudios.snapper.util.SnapperUtil; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.CubeMapRenderer; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.RotatingCubeMapRenderer; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.screen.ScreenTexts; -import net.minecraft.text.Text; -import net.minecraft.util.Colors; -import net.minecraft.util.Identifier; -import net.minecraft.util.Util; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.CubeMap; +import net.minecraft.client.renderer.PanoramaRenderer; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.CommonColors; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -23,41 +23,40 @@ import java.util.stream.Stream; public class PanoramaViewerScreen extends Screen { - protected static final Identifier ID = Snapper.id("screenshots/panorama"); - protected static final CubeMapRenderer PANORAMA_RENDERER = new CubeMapRenderer(ID); + protected static final ResourceLocation ID = Snapper.id("screenshots/panorama"); + protected static final CubeMap PANORAMA_RENDERER = new CubeMap(ID); - private final RotatingCubeMapRenderer rotatingPanoramaRenderer = new RotatingCubeMapRenderer(PANORAMA_RENDERER); + private final PanoramaRenderer rotatingPanoramaRenderer = new PanoramaRenderer(PANORAMA_RENDERER); private final DynamicCubemapTexture texture; private final String title; private final Screen parent; protected PanoramaViewerScreen(String title, Screen parent) { - super(Text.translatable("menu.snapper.viewer_menu")); + super(Component.translatable("menu.snapper.viewer_menu")); this.title = title; this.parent = parent; - this.client = MinecraftClient.getInstance(); - assert client != null; this.texture = this.getTexture(); + if (texture != null) { - client.getTextureManager().registerTexture(ID, texture); + // TODO: May be worth doing texture loading here off-thread as not to cause a freeze + Minecraft.getInstance().getTextureManager().registerAndLoad(ID, texture); } } @Nullable private DynamicCubemapTexture getTexture() { - assert client != null; + assert minecraft != null; Path panoramaDir = SnapperUtil.getConfiguredScreenshotDirectory().resolve("panorama"); if (!SnapperUtil.panoramaPresent(panoramaDir)) return null; try (Stream stream = Files.list(panoramaDir)) { - return stream - .allMatch(path -> { - if (Files.isDirectory(path)) return false; + return stream.allMatch(path -> { + if (Files.isDirectory(path)) return false; - return SafeFiles.isContentType(path, "image/png", ".png"); - }) ? DynamicCubemapTexture.createPanorama(ID, panoramaDir).orElse(null) : null; + return SafeFiles.isContentType(path, "image/png", ".png"); + }) ? DynamicCubemapTexture.createPanorama(ID, panoramaDir).orElse(null) : null; } catch (IOException | NullPointerException e) { Snapper.LOGGER.error("Failed to list the contents of directory", e); return null; @@ -65,55 +64,51 @@ private DynamicCubemapTexture getTexture() { } @Override - public void close() { - assert client != null; - + public void onClose() { if (texture != null) { - client.getTextureManager().destroyTexture(ID); + Minecraft.getInstance().getTextureManager().release(ID); texture.close(); } - client.setScreen(this.parent); + Minecraft.getInstance().setScreen(this.parent); } @Override protected void init() { // This is called whenever the window is resized. - assert client != null; - if (this.texture == null) { Snapper.LOGGER.error("No panorama found"); - close(); + onClose(); return; } - Path panoramaPath = Path.of(client.runDirectory.getPath(), "screenshots", "panorama"); - addDrawableChild(ButtonWidget.builder(Text.translatable("button.snapper.folder"), button -> { - Util.getOperatingSystem().open(panoramaPath); - }).dimensions(width / 2 - 150 - 4, height - 32, 150, 20).build()); + Path panoramaPath = Path.of(Minecraft.getInstance().gameDirectory.getPath(), "screenshots", "panorama"); + addRenderableWidget(Button.builder(Component.translatable("button.snapper.folder"), button -> { + Util.getPlatform().openPath(panoramaPath); + }).bounds(width / 2 - 150 - 4, height - 32, 150, 20).build()); - addDrawableChild(ButtonWidget.builder( - ScreenTexts.DONE, - button -> this.close() - ).dimensions(width / 2 + 4, height - 32, 150, 20).build()); + addRenderableWidget(Button.builder( + CommonComponents.GUI_DONE, + button -> this.onClose() + ).bounds(width / 2 + 4, height - 32, 150, 20).build()); } @Override - public void render(DrawContext context, int mouseX, int mouseY, float delta) { + public void render(GuiGraphics context, int mouseX, int mouseY, float delta) { rotatingPanoramaRenderer.render(context, this.width, this.height, true); - context.drawCenteredTextWithShadow( - this.textRenderer, + context.drawCenteredString( + this.font, this.title, this.width / 2, 20, - Colors.WHITE + CommonColors.WHITE ); super.render(context, mouseX, mouseY, delta); } @Override - public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) { + public void renderBackground(GuiGraphics context, int mouseX, int mouseY, float delta) { } } diff --git a/src/client/java/dev/spiritstudios/snapper/gui/screen/PrivacyNoticeScreen.java b/src/client/java/dev/spiritstudios/snapper/gui/screen/PrivacyNoticeScreen.java index 7ebbf47..c7d94d9 100644 --- a/src/client/java/dev/spiritstudios/snapper/gui/screen/PrivacyNoticeScreen.java +++ b/src/client/java/dev/spiritstudios/snapper/gui/screen/PrivacyNoticeScreen.java @@ -30,17 +30,19 @@ import dev.spiritstudios.snapper.SnapperConfig; import dev.spiritstudios.snapper.util.uploading.AxolotlClientApi; -import net.minecraft.client.font.MultilineText; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.screen.ScreenTexts; -import net.minecraft.text.Text; -import net.minecraft.util.Util; -import net.minecraft.util.math.MathHelper; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.MultiLineLabel; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.util.CommonColors; +import net.minecraft.util.Mth; +import org.jspecify.annotations.NonNull; import java.net.URI; -import java.util.Objects; import java.util.function.Consumer; public class PrivacyNoticeScreen extends Screen { @@ -48,65 +50,59 @@ public class PrivacyNoticeScreen extends Screen { private final Screen parent; private final Consumer accepted; - private MultilineText message; + private MultiLineLabel message; public PrivacyNoticeScreen(Screen parent, Consumer accepted) { - super(Text.translatable("snapper.privacy_notice")); + super(Component.translatable("snapper.privacy_notice")); this.parent = parent; this.accepted = accepted; } @Override - public void render(DrawContext graphics, int mouseX, int mouseY, float delta) { - super.render(graphics, mouseX, mouseY, delta); - graphics.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, getTitleY(), -1); - message.drawCenterWithShadow(graphics, width / 2, getMessageY()); + public void render(GuiGraphics context, int mouseX, int mouseY, float delta) { + super.render(context, mouseX, mouseY, delta); + context.drawCenteredString(this.font, this.title, this.width / 2, getTitleY(), CommonColors.WHITE); + message.render(context, MultiLineLabel.Align.CENTER, width / 2, getMessageY(), 10, true, CommonColors.WHITE); } @Override - public Text getNarratedTitle() { - return ScreenTexts.joinSentences(super.getNarratedTitle(), - Text.translatable("snapper.privacy_notice.description")); + public @NonNull Component getNarrationMessage() { + return CommonComponents.joinForNarration(super.getNarrationMessage(), + Component.translatable("snapper.privacy_notice.description")); } @Override protected void init() { - Objects.requireNonNull(client); + message = MultiLineLabel.create(Minecraft.getInstance().font, + Component.translatable("snapper.privacy_notice.description"), width - 50); - message = MultilineText.create(client.textRenderer, - Text.translatable("snapper.privacy_notice.description"), width - 50); - - int y = MathHelper.clamp(this.getMessageY() + this.getMessagesHeight() + 20, this.height / 6 + 96, this.height - 24); + int y = Mth.clamp(this.getMessageY() + this.getMessagesHeight() + 20, this.height / 6 + 96, this.height - 24); this.addButtons(y); } private void addButtons(int y) { - Objects.requireNonNull(client); - int buttonWidth = 120; - addDrawableChild(ButtonWidget.builder(Text.translatable("snapper.privacy_notice.view_terms"), buttonWidget -> - Util.getOperatingSystem().open(TERMS_URI)).dimensions(width / 2 - (buttonWidth / 2) - buttonWidth - 5, y, buttonWidth, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("snapper.privacy_notice.view_terms"), buttonWidget -> + Util.getPlatform().openUri(TERMS_URI)).bounds(width / 2 - (buttonWidth / 2) - buttonWidth - 5, y, buttonWidth, 20).build()); - addDrawableChild(ButtonWidget.builder(Text.translatable("snapper.privacy_notice.accept"), buttonWidget -> { - client.setScreen(parent); - SnapperConfig.INSTANCE.termsAccepted.set(AxolotlClientApi.TermsAcceptance.ACCEPTED); - SnapperConfig.HOLDER.save(); + addRenderableWidget(Button.builder(Component.translatable("snapper.privacy_notice.accept"), buttonWidget -> { + Minecraft.getInstance().setScreen(parent); + SnapperConfig.editAsync(m -> m.termsAccepted = AxolotlClientApi.TermsAcceptance.ACCEPTED); accepted.accept(true); - }).dimensions(width / 2 - (buttonWidth / 2), y, buttonWidth, 20).build()); + }).bounds(width / 2 - (buttonWidth / 2), y, buttonWidth, 20).build()); - addDrawableChild(ButtonWidget.builder(Text.translatable("snapper.privacy_notice.deny"), buttonWidget -> { - client.setScreen(parent); - SnapperConfig.INSTANCE.termsAccepted.set(AxolotlClientApi.TermsAcceptance.DENIED); - SnapperConfig.HOLDER.save(); + addRenderableWidget(Button.builder(Component.translatable("snapper.privacy_notice.deny"), buttonWidget -> { + Minecraft.getInstance().setScreen(parent); + SnapperConfig.editAsync(m -> m.termsAccepted = AxolotlClientApi.TermsAcceptance.DENIED); accepted.accept(false); - }).dimensions(width / 2 - (buttonWidth / 2) + buttonWidth + 5, y, buttonWidth, 20).build()); + }).bounds(width / 2 - (buttonWidth / 2) + buttonWidth + 5, y, buttonWidth, 20).build()); } private int getTitleY() { int i = (this.height - this.getMessagesHeight()) / 2; - return MathHelper.clamp(i - 20 - 9, 10, 80); + return Mth.clamp(i - 20 - 9, 10, 80); } private int getMessageY() { @@ -114,6 +110,6 @@ private int getMessageY() { } private int getMessagesHeight() { - return this.message.count() * 9; + return this.message.getLineCount() * 9; } } diff --git a/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotListScreen.java b/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotListScreen.java new file mode 100644 index 0000000..02b6280 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotListScreen.java @@ -0,0 +1,307 @@ +package dev.spiritstudios.snapper.gui.screen; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.serialization.Codec; +import dev.spiritstudios.snapper.Snapper; +import dev.spiritstudios.snapper.SnapperConfig; +import dev.spiritstudios.snapper.gui.toast.SnapperToast; +import dev.spiritstudios.snapper.gui.widget.ScreenshotListWidget; +import dev.spiritstudios.snapper.gui.widget.ScreenshotsWidget; +import dev.spiritstudios.snapper.util.PlatformHelper; +import dev.spiritstudios.snapper.util.ScreenshotActions; +import dev.spiritstudios.snapper.util.SnapperUtil; +import dev.spiritstudios.snapper.util.uploading.ScreenshotUploading; +import net.minecraft.Util; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.SpriteIconButton; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.layouts.EqualSpacingLayout; +import net.minecraft.client.gui.layouts.FrameLayout; +import net.minecraft.client.gui.layouts.LinearLayout; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.CommonColors; +import net.minecraft.util.StringRepresentable; +import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; + +import java.nio.file.Path; + +public class ScreenshotListScreen extends Screen { + private static final ResourceLocation PANORAMA_BUTTON_ICON = Snapper.id("screenshots/panorama"); + private static final ResourceLocation PANORAMA_BUTTON_DISABLED_ICON = Snapper.id("screenshots/panorama_disabled"); + + private static final ResourceLocation SETTINGS_ICON = Snapper.id("screenshots/settings"); + + private static final ResourceLocation VIEW_MODE_ICON_LIST = Snapper.id("screenshots/show_list"); + + private static final ResourceLocation VIEW_MODE_ICON_GRID = Snapper.id("screenshots/show_grid"); + + private final Screen parent; + private final boolean isOffline; + + private ScreenshotsWidget screenshots = null; + + private Button deleteButton; + private Button renameButton; + private Button viewButton; + private Button copyButton; + private Button openButton; + private Button uploadButton; + private SpriteIconButton viewModeButton; + private @Nullable ScreenshotListWidget.ScreenshotEntry selectedScreenshot = null; + + public ScreenshotListScreen(Screen parent) { + super(Component.translatable("menu.snapper.screenshot_menu")); + this.parent = parent; + this.isOffline = SnapperUtil.isOfflineAccount(); + } + + public synchronized void refresh() { + recreateList(); + recreateViewModeButton(); + } + + private void recreateList() { + if (screenshots != null) { + this.removeWidget(screenshots); + } + + screenshots = this.addRenderableWidget(ScreenshotsWidget.create( + minecraft, + width, + height - 48 - 68, + 48, + screenshots, + this + )); + } + + private void recreateViewModeButton() { + if (viewModeButton != null) { + removeWidget(this.viewModeButton); + } + + this.viewModeButton = addRenderableWidget(SpriteIconButton.builder( + Component.translatable("config.snapper.viewMode"), + button -> this.toggleGrid(), + true + ).width(20).sprite(SnapperConfig.HOLDER.get().viewMode() == ViewMode.LIST ? VIEW_MODE_ICON_LIST : VIEW_MODE_ICON_GRID, 15, 15).build()); + viewModeButton.setPosition(width / 2 - 178, height - 56); + } + + @Override + protected void init() { + assert minecraft != null; + + recreateList(); + + int secondRowButtonWidth = 100; + + Button folderButton = addRenderableWidget(Button.builder( + Component.translatable("button.snapper.folder"), + button -> Util.getPlatform().openPath(SnapperUtil.getConfiguredScreenshotDirectory()) + ).width(secondRowButtonWidth).build()); + + + this.openButton = addRenderableWidget(Button.builder( + Component.translatable("button.snapper.open"), + button -> { + if (selectedScreenshot != null) { + Util.getPlatform().openPath(selectedScreenshot.icon.getPath()); + } + } + ).width(secondRowButtonWidth).build()); + + Button doneButton = addRenderableWidget(Button.builder( + CommonComponents.GUI_DONE, + button -> this.onClose() + ).width(secondRowButtonWidth).build()); + + int firstRowButtonWidth = 58; + + this.deleteButton = addRenderableWidget(Button.builder( + Component.translatable("button.snapper.delete"), + button -> { + if (selectedScreenshot != null) { + ScreenshotActions.deleteScreenshot(selectedScreenshot.icon.getPath(), this); + } + } + ).width(firstRowButtonWidth).build()); + + this.renameButton = addRenderableWidget(Button.builder( + Component.translatable("button.snapper.rename"), + button -> { + if (this.selectedScreenshot != null) { + minecraft.setScreen(new ScreenshotRenameScreen(this.selectedScreenshot.icon.getPath(), this)); + } + } + ).width(firstRowButtonWidth).build()); + + this.copyButton = addRenderableWidget(Button.builder( + Component.translatable("button.snapper.copy"), + button -> { + if (selectedScreenshot != null) { + PlatformHelper.INSTANCE.copyScreenshot(selectedScreenshot.icon.getPath()); + } + } + ).width(firstRowButtonWidth).build()); + + this.viewButton = addRenderableWidget(Button.builder( + Component.translatable("button.snapper.view"), + button -> { + if (selectedScreenshot != null) { + this.minecraft.setScreen(new ScreenshotViewerScreen( + selectedScreenshot.icon, + selectedScreenshot.icon.getPath(), + selectedScreenshot.screenParent + )); + } + } + ).width(firstRowButtonWidth).build()); + + this.uploadButton = addRenderableWidget(Button.builder(Component.translatable("button.snapper.upload"), button -> { + if (selectedScreenshot == null) return; + + button.active = false; + ScreenshotUploading.upload(selectedScreenshot.icon.getPath()) + .thenRun(() -> button.active = true); + }).width(firstRowButtonWidth).build()); + + if (isOffline) { + this.uploadButton.setTooltip(Tooltip.create(Component.translatable("button.snapper.upload.tooltip"))); + } + + LinearLayout verticalButtonLayout = LinearLayout.vertical() + .spacing(4); + + EqualSpacingLayout firstRowWidget = verticalButtonLayout.addChild(new EqualSpacingLayout( + 308, + 20, + EqualSpacingLayout.Orientation.HORIZONTAL + )); + + firstRowWidget.addChild(this.deleteButton); + firstRowWidget.addChild(this.renameButton); + firstRowWidget.addChild(this.copyButton); + firstRowWidget.addChild(this.viewButton); + firstRowWidget.addChild(this.uploadButton); + + EqualSpacingLayout secondRowWidget = verticalButtonLayout.addChild(new EqualSpacingLayout( + 308, + 20, + EqualSpacingLayout.Orientation.HORIZONTAL + )); + + secondRowWidget.addChild(folderButton); + secondRowWidget.addChild(openButton); + secondRowWidget.addChild(doneButton); + + verticalButtonLayout.arrangeElements(); + FrameLayout.centerInRectangle(verticalButtonLayout, 0, this.height - 66, this.width, 64); + + SpriteIconButton settingsButton = addRenderableWidget(SpriteIconButton.builder( + Component.translatable("config.snapper.title"), + button -> this.minecraft.setScreen( + new ConfigScreen(new ScreenshotListScreen(this.parent))), + true + ).width(20).sprite(SETTINGS_ICON, 15, 15).build()); + + settingsButton.setPosition(width / 2 - 178, height - 32); + + recreateViewModeButton(); + + Path panoramaDir = SnapperUtil.getConfiguredScreenshotDirectory().resolve("panorama"); + boolean hasPanorama = SnapperUtil.panoramaPresent(panoramaDir); + + SpriteIconButton panoramaButton = addRenderableWidget(SpriteIconButton.builder( + Component.translatable("button.snapper.screenshots"), + button -> this.minecraft.setScreen(new PanoramaViewerScreen(Component.translatable("menu.snapper.panorama").getString(), this)), + true + ).width(20).sprite(hasPanorama ? PANORAMA_BUTTON_ICON : PANORAMA_BUTTON_DISABLED_ICON, 15, 15).build()); + + panoramaButton.active = hasPanorama; + panoramaButton.setPosition(width / 2 + 158, height - 32); + + panoramaButton.setTooltip(Tooltip.create(Component.translatable(hasPanorama ? + "button.snapper.panorama.tooltip" : + "text.snapper.panorama_encourage"))); + + + this.imageSelected(selectedScreenshot); + } + + public void imageSelected(@Nullable ScreenshotListWidget.ScreenshotEntry screenshot) { + boolean hasScreenshot = screenshot != null; + this.copyButton.active = hasScreenshot; + this.deleteButton.active = hasScreenshot; + this.openButton.active = hasScreenshot; + this.renameButton.active = hasScreenshot; + this.viewButton.active = hasScreenshot; + this.selectedScreenshot = screenshot; + this.uploadButton.active = !isOffline && hasScreenshot; + } + + public void toggleGrid() { + SnapperConfig.edit(m -> m.viewMode = SnapperConfig.HOLDER.get().viewMode() == ViewMode.GRID ? ViewMode.LIST : ViewMode.GRID); + + refresh(); + } + + @Override + public boolean keyPressed(KeyEvent input) { + assert minecraft != null; + + if (super.keyPressed(input)) { + return true; + } + + if (input.key() == InputConstants.KEY_F5) { + minecraft.setScreen(new ScreenshotListScreen(this.parent)); + return true; + } + + if (selectedScreenshot == null) return false; + + if ((input.modifiers() & InputConstants.MOD_CONTROL) != 0 && input.key() == InputConstants.KEY_C) { + PlatformHelper.INSTANCE.copyScreenshot(selectedScreenshot.icon.getPath()); + SnapperToast.push(SnapperToast.Type.SCREENSHOT, Component.translatable("toast.snapper.screenshot.copy"), null); + return true; + } + + if (input.key() == InputConstants.KEY_RETURN) { + minecraft.setScreen(new ScreenshotViewerScreen(selectedScreenshot.icon, selectedScreenshot.icon.getPath(), this)); + return true; + } + + return false; + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + super.render(graphics, mouseX, mouseY, delta); + graphics.drawCenteredString(this.font, this.title, this.width / 2, 20, CommonColors.WHITE); + } + + public enum ViewMode implements StringRepresentable { + LIST("list"), + GRID("grid"); + + public static final Codec CODEC = StringRepresentable.fromEnum(ViewMode::values); + + private final String name; + + ViewMode(String name) { + this.name = name; + } + + @Override + public @NonNull String getSerializedName() { + return name; + } + } +} diff --git a/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotRenameScreen.java b/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotRenameScreen.java index ed7494a..4997397 100644 --- a/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotRenameScreen.java +++ b/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotRenameScreen.java @@ -1,50 +1,50 @@ package dev.spiritstudios.snapper.gui.screen; import dev.spiritstudios.snapper.util.ScreenshotActions; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.font.TextRenderer; -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.gui.widget.TextWidget; -import net.minecraft.screen.ScreenTexts; -import net.minecraft.text.Text; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.StringWidget; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; import java.nio.file.Path; public class ScreenshotRenameScreen extends Screen { private final Path screenshot; - private final TextFieldWidget renameInput; - private final Text RENAME_INPUT_TEXT = Text.translatable("text.snapper.rename_input"); - private final MinecraftClient client = MinecraftClient.getInstance(); - private final TextRenderer textRenderer = client.textRenderer; + private final EditBox renameInput; + private final Component RENAME_INPUT_TEXT = Component.translatable("text.snapper.rename_input"); + private final Minecraft client = Minecraft.getInstance(); + private final Font textRenderer = client.font; private final Screen parent; protected ScreenshotRenameScreen(Path screenshot, Screen parent) { - super(Text.translatable("text.snapper.rename")); + super(Component.translatable("text.snapper.rename")); this.screenshot = screenshot; - this.renameInput = new TextFieldWidget(textRenderer, 200, 20, RENAME_INPUT_TEXT); + this.renameInput = new EditBox(textRenderer, 200, 20, RENAME_INPUT_TEXT); this.parent = parent; } @Override protected void init() { - this.addDrawableChild(new TextWidget(RENAME_INPUT_TEXT, textRenderer)) - .setPosition(this.width / 2 - textRenderer.getWidth(RENAME_INPUT_TEXT) / 2, this.height / 2 - 20); + this.addRenderableWidget(new StringWidget(RENAME_INPUT_TEXT, textRenderer)) + .setPosition(this.width / 2 - textRenderer.width(RENAME_INPUT_TEXT) / 2, this.height / 2 - 20); - this.addDrawableChild(this.renameInput).setPosition(this.width / 2 - 100, this.height / 2); + this.addRenderableWidget(this.renameInput).setPosition(this.width / 2 - 100, this.height / 2); - this.renameInput.setText(this.screenshot.getFileName().toString()); + this.renameInput.setValue(this.screenshot.getFileName().toString()); this.renameInput.setMaxLength(255); - this.addDrawableChild(ButtonWidget.builder( - Text.translatable("button.snapper.rename"), - button -> this.renameScreenshot(this.renameInput.getText()) - ).dimensions(width / 2 - 150 - 4, height - 32, 150, 20).build()); - - this.addDrawableChild(ButtonWidget.builder( - ScreenTexts.CANCEL, - button -> this.close() - ).dimensions(width / 2 + 4, height - 32, 150, 20).build()); + this.addRenderableWidget(Button.builder( + Component.translatable("button.snapper.rename"), + button -> this.renameScreenshot(this.renameInput.getValue()) + ).bounds(width / 2 - 150 - 4, height - 32, 150, 20).build()); + + this.addRenderableWidget(Button.builder( + CommonComponents.GUI_CANCEL, + button -> this.onClose() + ).bounds(width / 2 + 4, height - 32, 150, 20).build()); } private void renameScreenshot(String newName) { @@ -55,7 +55,7 @@ private void renameScreenshot(String newName) { } @Override - public void close() { + public void onClose() { this.client.setScreen(this.parent); } } diff --git a/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotScreen.java b/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotScreen.java deleted file mode 100644 index 36b9d36..0000000 --- a/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotScreen.java +++ /dev/null @@ -1,282 +0,0 @@ -package dev.spiritstudios.snapper.gui.screen; - -import dev.spiritstudios.snapper.Snapper; -import dev.spiritstudios.snapper.SnapperConfig; -import dev.spiritstudios.snapper.gui.widget.ScreenshotListWidget; -import dev.spiritstudios.snapper.util.ScreenshotActions; -import dev.spiritstudios.snapper.util.SnapperUtil; -import dev.spiritstudios.snapper.util.uploading.ScreenshotUploading; -import dev.spiritstudios.specter.api.config.client.RootConfigScreen; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.tooltip.Tooltip; -import net.minecraft.client.gui.widget.AxisGridWidget; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.gui.widget.DirectionalLayoutWidget; -import net.minecraft.client.gui.widget.SimplePositioningWidget; -import net.minecraft.client.gui.widget.TextIconButtonWidget; -import net.minecraft.client.util.InputUtil; -import net.minecraft.screen.ScreenTexts; -import net.minecraft.text.Text; -import net.minecraft.util.Colors; -import net.minecraft.util.Identifier; -import net.minecraft.util.Util; -import org.jetbrains.annotations.Nullable; -import org.lwjgl.glfw.GLFW; - -import java.nio.file.Path; - -public class ScreenshotScreen extends Screen { - private static final Identifier PANORAMA_BUTTON_ICON = Snapper.id("screenshots/panorama"); - private static final Identifier PANORAMA_BUTTON_DISABLED_ICON = Snapper.id("screenshots/panorama_disabled"); - - private static final Identifier SETTINGS_ICON = Snapper.id("screenshots/settings"); - - private static final Identifier VIEW_MODE_ICON_LIST = Snapper.id("screenshots/show_list"); - - private static final Identifier VIEW_MODE_ICON_GRID = Snapper.id("screenshots/show_grid"); - - private final Screen parent; - private final boolean isOffline; - - private ScreenshotListWidget screenshotList; - private ButtonWidget deleteButton; - private ButtonWidget renameButton; - private ButtonWidget viewButton; - private ButtonWidget copyButton; - private ButtonWidget openButton; - private ButtonWidget uploadButton; - private TextIconButtonWidget viewModeButton; - private @Nullable ScreenshotListWidget.ScreenshotEntry selectedScreenshot = null; - private boolean showGrid; - - public ScreenshotScreen(Screen parent) { - super(Text.translatable("menu.snapper.screenshot_menu")); - this.parent = parent; - - this.showGrid = SnapperConfig.INSTANCE.viewMode.get().equals(ViewMode.GRID); - this.isOffline = SnapperUtil.isOfflineAccount(); - } - - @Override - protected void init() { - if (client == null) return; - screenshotList = this.addDrawableChild(new ScreenshotListWidget( - client, - width, - height - 48 - 68, - 48, - 36, - screenshotList, - this - )); - - int secondRowButtonWidth = 100; - - ButtonWidget folderButton = addDrawableChild(ButtonWidget.builder( - Text.translatable("button.snapper.folder"), - button -> Util.getOperatingSystem().open(SnapperUtil.getConfiguredScreenshotDirectory()) - ).width(secondRowButtonWidth).build()); - - - this.openButton = addDrawableChild(ButtonWidget.builder( - Text.translatable("button.snapper.open"), - button -> { - if (selectedScreenshot != null) { - Util.getOperatingSystem().open(selectedScreenshot.icon.getPath()); - } - } - ).width(secondRowButtonWidth).build()); - - ButtonWidget doneButton = addDrawableChild(ButtonWidget.builder( - ScreenTexts.DONE, - button -> this.close() - ).width(secondRowButtonWidth).build()); - - int firstRowButtonWidth = 58; - - this.deleteButton = addDrawableChild(ButtonWidget.builder( - Text.translatable("button.snapper.delete"), - button -> { - if (selectedScreenshot != null) { - ScreenshotActions.deleteScreenshot(selectedScreenshot.icon.getPath(), this); - } - } - ).width(firstRowButtonWidth).build()); - - this.renameButton = addDrawableChild(ButtonWidget.builder( - Text.translatable("button.snapper.rename"), - button -> { - if (this.selectedScreenshot != null) { - client.setScreen(new ScreenshotRenameScreen(this.selectedScreenshot.icon.getPath(), this)); - } - } - ).width(firstRowButtonWidth).build()); - - this.copyButton = addDrawableChild(ButtonWidget.builder( - Text.translatable("button.snapper.copy"), - button -> { - if (selectedScreenshot != null) { - Snapper.getPlatformHelper().copyScreenshot(selectedScreenshot.icon.getPath()); - } - } - ).width(firstRowButtonWidth).build()); - - this.viewButton = addDrawableChild(ButtonWidget.builder( - Text.translatable("button.snapper.view"), - button -> { - if (selectedScreenshot != null) { - this.client.setScreen(new ScreenshotViewerScreen( - selectedScreenshot.icon, - selectedScreenshot.icon.getPath(), - selectedScreenshot.screenParent - )); - } - } - ).width(firstRowButtonWidth).build()); - - this.uploadButton = addDrawableChild(ButtonWidget.builder(Text.translatable("button.snapper.upload"), button -> { - if (selectedScreenshot == null) return; - - button.active = false; - ScreenshotUploading.upload(selectedScreenshot.icon.getPath()) - .thenRun(() -> button.active = true); - }).width(firstRowButtonWidth).build()); - - if (isOffline) { - this.uploadButton.setTooltip(Tooltip.of(Text.translatable("button.snapper.upload.tooltip"))); - } - - DirectionalLayoutWidget verticalButtonLayout = DirectionalLayoutWidget.vertical() - .spacing(4); - - AxisGridWidget firstRowWidget = verticalButtonLayout.add(new AxisGridWidget( - 308, - 20, - AxisGridWidget.DisplayAxis.HORIZONTAL - )); - - firstRowWidget.add(this.deleteButton); - firstRowWidget.add(this.renameButton); - firstRowWidget.add(this.copyButton); - firstRowWidget.add(this.viewButton); - firstRowWidget.add(this.uploadButton); - - AxisGridWidget secondRowWidget = verticalButtonLayout.add(new AxisGridWidget( - 308, - 20, - AxisGridWidget.DisplayAxis.HORIZONTAL - )); - - secondRowWidget.add(folderButton); - secondRowWidget.add(this.openButton); - secondRowWidget.add(doneButton); - - verticalButtonLayout.refreshPositions(); - SimplePositioningWidget.setPos(verticalButtonLayout, 0, this.height - 66, this.width, 64); - - TextIconButtonWidget settingsButton = addDrawableChild(TextIconButtonWidget.builder( - Text.translatable("config.snapper.snapper.title"), - button -> this.client.setScreen( - new RootConfigScreen(SnapperConfig.HOLDER, new ScreenshotScreen(this.parent))), - true - ).width(20).texture(SETTINGS_ICON, 15, 15).build()); - - settingsButton.setPosition(width / 2 - 178, height - 32); - - - this.viewModeButton = addDrawableChild(TextIconButtonWidget.builder( - Text.translatable("config.snapper.snapper.viewMode"), - button -> this.toggleGrid(), - true - ).width(20).texture(showGrid ? VIEW_MODE_ICON_LIST : VIEW_MODE_ICON_GRID, 15, 15).build()); - - viewModeButton.setPosition(width / 2 - 178, height - 56); - - Path panoramaDir = SnapperUtil.getConfiguredScreenshotDirectory().resolve("panorama"); - boolean hasPanorama = SnapperUtil.panoramaPresent(panoramaDir); - - TextIconButtonWidget panoramaButton = addDrawableChild(TextIconButtonWidget.builder( - Text.translatable("button.snapper.screenshots"), - button -> this.client.setScreen(new PanoramaViewerScreen(Text.translatable("menu.snapper.panorama").getString(), this)), - true - ).width(20).texture(hasPanorama ? PANORAMA_BUTTON_ICON : PANORAMA_BUTTON_DISABLED_ICON, 15, 15).build()); - - panoramaButton.active = hasPanorama; - panoramaButton.setPosition(width / 2 + 158, height - 32); - - panoramaButton.setTooltip(Tooltip.of(Text.translatable(hasPanorama ? - "button.snapper.panorama.tooltip" : - "text.snapper.panorama_encourage"))); - - - this.imageSelected(selectedScreenshot); - } - - public void imageSelected(@Nullable ScreenshotListWidget.ScreenshotEntry screenshot) { - boolean hasScreenshot = screenshot != null; - this.copyButton.active = hasScreenshot; - this.deleteButton.active = hasScreenshot; - this.openButton.active = hasScreenshot; - this.renameButton.active = hasScreenshot; - this.viewButton.active = hasScreenshot; - this.selectedScreenshot = screenshot; - this.uploadButton.active = !isOffline && hasScreenshot; - } - - public void toggleGrid() { - screenshotList.toggleGrid(); - screenshotList.refreshScroll(); - this.showGrid = !this.showGrid; - - remove(this.viewModeButton); - this.viewModeButton = addDrawableChild(TextIconButtonWidget.builder( - Text.translatable("config.snapper.snapper.viewMode"), - button -> this.toggleGrid(), - true - ).width(20).texture(showGrid ? VIEW_MODE_ICON_LIST : VIEW_MODE_ICON_GRID, 15, 15).build()); - viewModeButton.setPosition(width / 2 - 178, height - 56); - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (super.keyPressed(keyCode, scanCode, modifiers)) return true; - if (client == null) return false; - - if (keyCode == GLFW.GLFW_KEY_F5) { - client.setScreen(new ScreenshotScreen(this.parent)); - return true; - } - - if (selectedScreenshot == null) return false; - - if ((modifiers & GLFW.GLFW_MOD_CONTROL) != 0 && keyCode == InputUtil.GLFW_KEY_C) { - Snapper.getPlatformHelper().copyScreenshot(selectedScreenshot.icon.getPath()); - return true; - } - - if (keyCode == GLFW.GLFW_KEY_ENTER) { - client.setScreen(new ScreenshotViewerScreen(selectedScreenshot.icon, selectedScreenshot.icon.getPath(), this)); - return true; - } - - return false; - } - - @Override - public void close() { - SnapperConfig.HOLDER.save(); - super.close(); - } - - @Override - public void render(DrawContext context, int mouseX, int mouseY, float delta) { - super.render(context, mouseX, mouseY, delta); - context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, Colors.WHITE); - } - - public enum ViewMode { - LIST, - GRID - } -} diff --git a/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotViewerScreen.java b/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotViewerScreen.java index 2d45452..2ec0567 100644 --- a/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotViewerScreen.java +++ b/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotViewerScreen.java @@ -1,25 +1,27 @@ package dev.spiritstudios.snapper.gui.screen; import dev.spiritstudios.snapper.Snapper; -import dev.spiritstudios.snapper.util.DynamicTexture; +import dev.spiritstudios.snapper.util.PlatformHelper; import dev.spiritstudios.snapper.util.ScreenshotActions; +import dev.spiritstudios.snapper.util.ScreenshotTexture; import dev.spiritstudios.snapper.util.SnapperUtil; import dev.spiritstudios.snapper.util.uploading.ScreenshotUploading; import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gl.RenderPipelines; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.tooltip.Tooltip; -import net.minecraft.client.gui.widget.AxisGridWidget; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.gui.widget.DirectionalLayoutWidget; -import net.minecraft.client.gui.widget.SimplePositioningWidget; -import net.minecraft.screen.ScreenTexts; -import net.minecraft.text.Text; -import net.minecraft.util.Colors; -import net.minecraft.util.Identifier; -import net.minecraft.util.Util; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.layouts.EqualSpacingLayout; +import net.minecraft.client.gui.layouts.FrameLayout; +import net.minecraft.client.gui.layouts.HeaderAndFooterLayout; +import net.minecraft.client.gui.layouts.LinearLayout; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.CommonColors; import org.jetbrains.annotations.Nullable; import javax.imageio.ImageIO; @@ -30,16 +32,13 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import java.util.concurrent.CompletableFuture; - -import static org.lwjgl.glfw.GLFW.*; public class ScreenshotViewerScreen extends Screen { - private static final Identifier MENU_DECOR_BACKGROUND_TEXTURE = Identifier.ofVanilla("textures/gui/menu_list_background.png"); - private static final Identifier INWORLD_MENU_DECOR_BACKGROUND_TEXTURE = Identifier.ofVanilla("textures/gui/inworld_menu_list_background.png"); + private static final ResourceLocation MENU_DECOR_BACKGROUND_TEXTURE = ResourceLocation.withDefaultNamespace("textures/gui/menu_list_background.png"); + private static final ResourceLocation INWORLD_MENU_DECOR_BACKGROUND_TEXTURE = ResourceLocation.withDefaultNamespace("textures/gui/inworld_menu_list_background.png"); - private final MinecraftClient client = MinecraftClient.getInstance(); - private final DynamicTexture image; + private final Minecraft client = Minecraft.getInstance(); + private final ScreenshotTexture image; private final String title; private final int imageWidth; private final int imageHeight; @@ -48,13 +47,14 @@ public class ScreenshotViewerScreen extends Screen { private final @Nullable List screenshots; private final int screenshotIndex; private final Path iconPath; + private final HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this); - public ScreenshotViewerScreen(DynamicTexture icon, Path screenshot, Screen parent) { + public ScreenshotViewerScreen(ScreenshotTexture icon, Path screenshot, Screen parent) { this(icon, screenshot, parent, null); } - public ScreenshotViewerScreen(DynamicTexture icon, Path iconPath, Screen parent, @Nullable List screenshots) { - super(Text.translatable("menu.snapper.viewer_menu")); + public ScreenshotViewerScreen(ScreenshotTexture icon, Path iconPath, Screen parent, @Nullable List screenshots) { + super(Component.translatable("menu.snapper.viewer_menu")); this.parent = parent; this.iconPath = iconPath; @@ -80,7 +80,7 @@ public ScreenshotViewerScreen(DynamicTexture icon, Path iconPath, Screen parent, } @Override - public void close() { + public void onClose() { this.client.setScreen(this.parent); } @@ -91,9 +91,9 @@ protected void init() { // OPEN FOLDER - ButtonWidget folderButton = addDrawableChild(ButtonWidget.builder( - Text.translatable("button.snapper.folder"), - button -> Util.getOperatingSystem().open(new File(client.runDirectory, "screenshots")) + Button folderButton = addRenderableWidget(Button.builder( + Component.translatable("button.snapper.folder"), + button -> Util.getPlatform().openFile(new File(client.gameDirectory, "screenshots")) ) .width(100) .build() @@ -101,29 +101,29 @@ protected void init() { // OPEN IMAGE EXTERNALLY - ButtonWidget openButton = addDrawableChild(ButtonWidget.builder( - Text.translatable("button.snapper.open"), - button -> Util.getOperatingSystem().open(this.iconPath) + Button openButton = addRenderableWidget(Button.builder( + Component.translatable("button.snapper.open"), + button -> Util.getPlatform().openPath(this.iconPath) ).width(100).build()); // EXIT PAGE - ButtonWidget doneButton = addDrawableChild(ButtonWidget.builder( - ScreenTexts.DONE, - button -> this.close() + Button doneButton = addRenderableWidget(Button.builder( + CommonComponents.GUI_DONE, + button -> this.onClose() ).width(100).build()); // DELETE SCREENSHOT - ButtonWidget deleteButton = addDrawableChild(ButtonWidget.builder( - Text.translatable("button.snapper.delete"), + Button deleteButton = addRenderableWidget(Button.builder( + Component.translatable("button.snapper.delete"), button -> ScreenshotActions.deleteScreenshot(this.screenshot, this.parent) ).width(firstRowButtonWidth).build()); // RENAME SCREENSHOT - ButtonWidget renameButton = addDrawableChild(ButtonWidget.builder( - Text.translatable("button.snapper.rename"), + Button renameButton = addRenderableWidget(Button.builder( + Component.translatable("button.snapper.rename"), button -> { if (this.screenshot != null) client.setScreen(new ScreenshotRenameScreen(this.screenshot, this.parent)); @@ -132,15 +132,15 @@ protected void init() { // COPY SCREENSHOT - ButtonWidget copyButton = addDrawableChild(ButtonWidget.builder( - Text.translatable("button.snapper.copy"), - button -> Snapper.getPlatformHelper().copyScreenshot(this.screenshot) + Button copyButton = addRenderableWidget(Button.builder( + Component.translatable("button.snapper.copy"), + button -> PlatformHelper.INSTANCE.copyScreenshot(this.screenshot) ).width(firstRowButtonWidth).build()); // UPLOAD SCREENSHOT - ButtonWidget uploadButton = addDrawableChild(ButtonWidget.builder( - Text.translatable("button.snapper.upload"), + Button uploadButton = addRenderableWidget(Button.builder( + Component.translatable("button.snapper.upload"), button -> { button.active = false; ScreenshotUploading.upload(iconPath).thenRun(() -> button.active = true); @@ -149,112 +149,109 @@ protected void init() { if (SnapperUtil.isOfflineAccount()) { uploadButton.active = false; - uploadButton.setTooltip(Tooltip.of(Text.translatable("button.snapper.upload.tooltip"))); + uploadButton.setTooltip(Tooltip.create(Component.translatable("button.snapper.upload.tooltip"))); } - DirectionalLayoutWidget verticalButtonLayout = DirectionalLayoutWidget.vertical().spacing(4); + LinearLayout verticalButtonLayout = LinearLayout.vertical().spacing(4); - AxisGridWidget firstRowWidget = verticalButtonLayout.add(new AxisGridWidget( + EqualSpacingLayout firstRowWidget = verticalButtonLayout.addChild(new EqualSpacingLayout( 308, 20, - AxisGridWidget.DisplayAxis.HORIZONTAL) + EqualSpacingLayout.Orientation.HORIZONTAL) ); - firstRowWidget.add(deleteButton); - firstRowWidget.add(renameButton); - firstRowWidget.add(copyButton); - firstRowWidget.add(uploadButton); + firstRowWidget.addChild(deleteButton); + firstRowWidget.addChild(renameButton); + firstRowWidget.addChild(copyButton); + firstRowWidget.addChild(uploadButton); - AxisGridWidget secondRowWidget = verticalButtonLayout.add(new AxisGridWidget( + EqualSpacingLayout secondRowWidget = verticalButtonLayout.addChild(new EqualSpacingLayout( 308, 20, - AxisGridWidget.DisplayAxis.HORIZONTAL) + EqualSpacingLayout.Orientation.HORIZONTAL) ); - secondRowWidget.add(folderButton); - secondRowWidget.add(openButton); - secondRowWidget.add(doneButton); + secondRowWidget.addChild(folderButton); + secondRowWidget.addChild(openButton); + secondRowWidget.addChild(doneButton); + + verticalButtonLayout.arrangeElements(); + FrameLayout.centerInRectangle(verticalButtonLayout, 0, this.height - 66, this.width, 64); - verticalButtonLayout.refreshPositions(); - SimplePositioningWidget.setPos(verticalButtonLayout, 0, this.height - 66, this.width, 64); + layout.setHeaderHeight(46); + layout.setFooterHeight(height - 68); } @Override - public void render(DrawContext context, int mouseX, int mouseY, float delta) { + public void render(GuiGraphics context, int mouseX, int mouseY, float delta) { super.render(context, mouseX, mouseY, delta); this.drawMenuBackground(context); this.drawHeaderAndFooterSeparators(context); - context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, Colors.WHITE); + context.drawCenteredString(this.font, this.title, this.width / 2, 20, CommonColors.WHITE); - int finalHeight = this.height - 48 - 68; + int finalHeight = this.height - 50 - 68; float scaleFactor = (float) finalHeight / imageHeight; int finalWidth = (int) (imageWidth * scaleFactor); - context.drawTexture( + context.blit( RenderPipelines.GUI_TEXTURED, this.image.getTextureId(), - (this.width / 2) - (finalWidth / 2), this.height - 68 - finalHeight, + (this.width / 2) - (finalWidth / 2), this.height - 70 - finalHeight, 0, 0, finalWidth, finalHeight, finalWidth, finalHeight ); if (screenshotIndex != -1 && screenshots != null) { - context.drawCenteredTextWithShadow( - this.textRenderer, + context.drawCenteredString( + this.font, "Screenshot %d/%d".formatted(screenshotIndex + 1, screenshots.size()), this.width / 2, 30, - Colors.WHITE + CommonColors.WHITE ); } - if (FabricLoader.getInstance().isDevelopmentEnvironment()) renderDebugInfo(context); - } - - private void renderDebugInfo(DrawContext context) { - context.getMatrices().pushMatrix(); - int finalHeight = this.height - 48 - 48; - float scaleFactor = (float) finalHeight / imageHeight; - int finalWidth = (int) (imageWidth * scaleFactor); - - context.drawCenteredTextWithShadow( - this.textRenderer, - "Image Size: %dx%d".formatted(imageWidth, imageHeight), - this.width / 2, - 40, - Colors.WHITE - ); + // TODO: Maybe add an option to the debug menu to turn this off + if (FabricLoader.getInstance().isDevelopmentEnvironment()) { + context.drawCenteredString( + this.font, + Component.translatable("text.snapper.image_size", imageWidth, imageHeight), + this.width / 2, + 40, + CommonColors.WHITE + ); - context.drawCenteredTextWithShadow( - this.textRenderer, - "Screen Size: %dx%d".formatted(this.width, this.height), - this.width / 2, - 50, - Colors.WHITE - ); + context.drawCenteredString( + this.font, + Component.translatable("text.snapper.screen_size", this.width, this.height), + this.width / 2, + 50, + CommonColors.WHITE + ); - context.drawCenteredTextWithShadow(this.textRenderer, - "Scale Factor: %s".formatted(scaleFactor), - this.width / 2, - 60, - Colors.WHITE - ); + context.drawCenteredString(this.font, + Component.translatable("text.snapper.scale_factor", scaleFactor), + this.width / 2, + 60, + CommonColors.WHITE + ); - context.drawCenteredTextWithShadow( - this.textRenderer, - "Scaled Size: %dx%d".formatted(finalWidth, finalHeight), - this.width / 2, - 70, - Colors.WHITE - ); + context.drawCenteredString( + this.font, + Component.translatable("text.snapper.scale_size", finalWidth, finalHeight), + this.width / 2, + 70, + CommonColors.WHITE + ); + } } - private void drawMenuBackground(DrawContext context) { - context.drawTexture( + private void drawMenuBackground(GuiGraphics context) { + context.blit( RenderPipelines.GUI_TEXTURED, - this.client.world == null ? + this.client.level == null ? MENU_DECOR_BACKGROUND_TEXTURE : INWORLD_MENU_DECOR_BACKGROUND_TEXTURE, 0, @@ -268,56 +265,27 @@ private void drawMenuBackground(DrawContext context) { ); } - private void drawHeaderAndFooterSeparators(DrawContext context) { - context.drawTexture( + private void drawHeaderAndFooterSeparators(GuiGraphics context) { + context.blit( RenderPipelines.GUI_TEXTURED, - this.client.world == null ? - Screen.HEADER_SEPARATOR_TEXTURE : - Screen.INWORLD_HEADER_SEPARATOR_TEXTURE, - 0, 48 - 2, + this.client.level == null ? + Screen.HEADER_SEPARATOR : + Screen.INWORLD_HEADER_SEPARATOR, + 0, layout.getHeaderHeight(), 0, 0, width, 2, 32, 2 ); - context.drawTexture( + context.blit( RenderPipelines.GUI_TEXTURED, - this.client.world == null ? - Screen.FOOTER_SEPARATOR_TEXTURE : - Screen.INWORLD_FOOTER_SEPARATOR_TEXTURE, - 0, height - 68, + this.client.level == null ? + Screen.FOOTER_SEPARATOR : + Screen.INWORLD_FOOTER_SEPARATOR, + 0, this.layout.getFooterHeight() - 2, 0, 0, width, 2, 32, 2 ); } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (this.screenshotIndex == -1 || this.screenshots == null) - return super.keyPressed(keyCode, scanCode, modifiers); - - Path imagePath = switch (keyCode) { - case GLFW_KEY_LEFT -> this.screenshotIndex >= 1 ? - screenshots.get(screenshotIndex - 1) : - screenshots.getLast(); - case GLFW_KEY_RIGHT -> this.screenshotIndex < this.screenshots.size() - 1 ? - screenshots.get(screenshotIndex + 1) : - screenshots.getFirst(); - default -> null; - }; - - if (imagePath == null) return super.keyPressed(keyCode, scanCode, modifiers); - CompletableFuture.supplyAsync(() -> DynamicTexture.createScreenshot(client.getTextureManager(), imagePath), Util.getIoWorkerExecutor()) - .thenAccept(texture -> texture.ifPresent(dynamicTexture -> client.submit(() -> { - client.setScreen(new ScreenshotViewerScreen( - dynamicTexture, imagePath, - this.parent, - this.screenshots - )); - dynamicTexture.load(); - }))); - - return super.keyPressed(keyCode, scanCode, modifiers); - } } diff --git a/src/client/java/dev/spiritstudios/snapper/gui/toast/SnapperToast.java b/src/client/java/dev/spiritstudios/snapper/gui/toast/SnapperToast.java index aa13550..219819d 100644 --- a/src/client/java/dev/spiritstudios/snapper/gui/toast/SnapperToast.java +++ b/src/client/java/dev/spiritstudios/snapper/gui/toast/SnapperToast.java @@ -1,51 +1,62 @@ package dev.spiritstudios.snapper.gui.toast; import dev.spiritstudios.snapper.Snapper; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.font.TextRenderer; -import net.minecraft.client.gl.RenderPipelines; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.toast.Toast; -import net.minecraft.client.toast.ToastManager; -import net.minecraft.text.OrderedText; -import net.minecraft.text.Text; -import net.minecraft.util.Colors; -import net.minecraft.util.Identifier; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.toasts.Toast; +import net.minecraft.client.gui.components.toasts.ToastManager; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.CommonColors; +import net.minecraft.util.FormattedCharSequence; +import org.jspecify.annotations.NonNull; import java.util.List; public class SnapperToast implements Toast { - private static final Identifier TEXTURE = Snapper.id("toast/snapper"); - private static final Identifier SCREENSHOT_ICON = Snapper.id("icon/image"); - private static final Identifier PANORAMA_ICON = Snapper.id("icon/panorama"); - private static final Identifier UPLOAD_ICON = Snapper.id("icon/upload"); - private static final Identifier DENY_ICON = Snapper.id("icon/nuh_uh"); + private static final ResourceLocation TEXTURE = Snapper.id("toast/snapper"); + private static final ResourceLocation SCREENSHOT_ICON = Snapper.id("icon/image"); + private static final ResourceLocation PANORAMA_ICON = Snapper.id("icon/panorama"); + private static final ResourceLocation UPLOAD_ICON = Snapper.id("icon/upload"); + private static final ResourceLocation DENY_ICON = Snapper.id("icon/nuh_uh"); private static final int VISIBILITY_DURATION = 5000; private static final int WIDTH = 256; private static final int LINE_HEIGHT = 12; private static final int PADDING = 10; private final Type type; - private final Text title; - private final List lines; + private final Component title; + private final List lines; private Visibility visibility; - public SnapperToast(Type type, Text title, Text description) { + public SnapperToast(Type type, Component title, Component description) { this.type = type; this.visibility = Visibility.HIDE; this.title = title; - MinecraftClient client = MinecraftClient.getInstance(); - TextRenderer textRenderer = client.textRenderer; - this.lines = textRenderer.wrapLines(description, WIDTH - PADDING * 3 - 16); + Minecraft minecraft = Minecraft.getInstance(); + Font textRenderer = minecraft.font; + this.lines = textRenderer.split(description, WIDTH - PADDING * 3 - 16); + } + + public static void push(Type type, Component title, Component description) { + Minecraft.getInstance().getToastManager().addToast( + new SnapperToast( + type, + title, + description + ) + ); } @Override - public Visibility getVisibility() { + public @NonNull Visibility getWantedVisibility() { return this.visibility; } - public Visibility setVisibility(Visibility visibility) { + public Visibility setWantedVisibility(Visibility visibility) { return this.visibility = visibility; } @@ -55,26 +66,26 @@ public void update(ToastManager manager, long time) { } @Override - public void draw(DrawContext context, TextRenderer textRenderer, long startTime) { - context.drawGuiTexture(RenderPipelines.GUI_TEXTURED, TEXTURE, 0, 0, this.getWidth(), this.getHeight()); - context.drawGuiTexture(RenderPipelines.GUI_TEXTURED, getCurrentTexture(), PADDING, PADDING, 16, 16); + public void render(GuiGraphics context, Font font, long startTime) { + context.blitSprite(RenderPipelines.GUI_TEXTURED, TEXTURE, 0, 0, this.width(), this.height()); + context.blitSprite(RenderPipelines.GUI_TEXTURED, getCurrentComponenture(), PADDING, PADDING, 16, 16); - context.drawText(textRenderer, title, PADDING * 2 + 14, this.lines.isEmpty() ? 12 : 7, Colors.YELLOW, false); + context.drawString(font, title, PADDING * 2 + 12, this.lines.isEmpty() ? 12 : 7, CommonColors.YELLOW, false); for (int i = 0; i < this.lines.size(); ++i) { - context.drawText(textRenderer, this.lines.get(i), PADDING * 2 + 14, 18 + i * 12, Colors.WHITE, false); + context.drawString(font, this.lines.get(i), PADDING * 2 + 12, 18 + i * 12, CommonColors.WHITE, false); } } - public int getWidth() { + public int width() { return WIDTH; } - public int getHeight() { + public int height() { return PADDING * 2 + Math.max(this.lines.size(), 1) * LINE_HEIGHT; } - private Identifier getCurrentTexture() { + private ResourceLocation getCurrentComponenture() { return switch (type) { case UPLOAD -> UPLOAD_ICON; case PANORAMA -> PANORAMA_ICON; diff --git a/src/client/java/dev/spiritstudios/snapper/gui/widget/ConfigList.java b/src/client/java/dev/spiritstudios/snapper/gui/widget/ConfigList.java new file mode 100644 index 0000000..4fc6fe2 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/gui/widget/ConfigList.java @@ -0,0 +1,134 @@ +package dev.spiritstudios.snapper.gui.widget; + +import dev.spiritstudios.snapper.gui.screen.ConfigScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.ContainerObjectSelectionList; +import net.minecraft.client.gui.components.StringWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; + +import java.util.List; + +public class ConfigList extends ContainerObjectSelectionList { + private static final int BIG_BUTTON_WIDTH = 310; + private static final int DEFAULT_ITEM_HEIGHT = 25; + + private final ConfigScreen screen; + + public ConfigList(Minecraft minecraft, int width, ConfigScreen screen) { + super(minecraft, width, screen.layout.getContentHeight(), screen.layout.getHeaderHeight(), DEFAULT_ITEM_HEIGHT); + + this.centerListVertically = true; + this.screen = screen; + } + + public void addBig(AbstractWidget option) { + this.addEntry(Entry.big(option, this.screen)); + } + + public void addSmall(AbstractWidget... options) { + for (int i = 0; i < options.length; i += 2) { + this.addSmall(options[i], i < options.length - 1 ? options[i + 1] : null); + } + } + + public void addSmall(AbstractWidget leftOption, @Nullable AbstractWidget rightOption) { + this.addEntry(Entry.small(leftOption, rightOption, this.screen)); + } + + + public void addHeader(Component text) { + int lineHeight = 9; + int paddingTop = this.children().isEmpty() ? 0 : lineHeight * 2; + this.addEntry(new HeaderEntry(this.screen, text, paddingTop), paddingTop + lineHeight + 4); + } + + @Override + public int getRowWidth() { + return BIG_BUTTON_WIDTH; + } + + protected abstract static class AbstractEntry extends ContainerObjectSelectionList.Entry { + } + + public static class Entry extends AbstractEntry { + private static final int X_OFFSET = 160; + + private final List children; + private final Screen screen; + + Entry(List children, Screen screen) { + this.children = List.copyOf(children); + this.screen = screen; + } + + public static Entry big(AbstractWidget option, Screen screen) { + option.setWidth(310); + return new Entry(List.of(option), screen); + } + + public static Entry small(AbstractWidget leftOption, @Nullable AbstractWidget rightOption, Screen screen) { + leftOption.setWidth(150); + if (rightOption != null) rightOption.setWidth(150); + + return rightOption == null + ? new Entry(List.of(leftOption), screen) + : new Entry(List.of(leftOption, rightOption), screen); + } + + @Override + public void renderContent(GuiGraphics guiGraphics, int mouseX, int mouseY, boolean isHovering, float partialTick) { + int xOffset = 0; + int x = this.screen.width / 2 - 155; + + for (AbstractWidget abstractWidget : this.children) { + abstractWidget.setPosition(x + xOffset, this.getContentY()); + abstractWidget.render(guiGraphics, mouseX, mouseY, partialTick); + xOffset += X_OFFSET; + } + } + + @Override + public @NonNull List narratables() { + return children; + } + + @Override + public @NonNull List children() { + return children; + } + } + + protected static class HeaderEntry extends AbstractEntry { + private final Screen screen; + private final int paddingTop; + private final StringWidget widget; + + protected HeaderEntry(final Screen screen, final Component text, final int paddingTop) { + this.screen = screen; + this.paddingTop = paddingTop; + this.widget = new StringWidget(text, screen.getFont()); + } + + @Override + public @NonNull List narratables() { + return List.of(this.widget); + } + + @Override + public void renderContent(final GuiGraphics graphics, final int mouseX, final int mouseY, final boolean hovered, final float a) { + this.widget.setPosition(this.screen.width / 2 - 155, this.getContentY() + this.paddingTop); + this.widget.render(graphics, mouseX, mouseY, a); + } + + public @NonNull List children() { + return List.of(this.widget); + } + } +} diff --git a/src/client/java/dev/spiritstudios/snapper/gui/widget/ConfigSliderWidget.java b/src/client/java/dev/spiritstudios/snapper/gui/widget/ConfigSliderWidget.java new file mode 100644 index 0000000..a2df146 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/gui/widget/ConfigSliderWidget.java @@ -0,0 +1,74 @@ +package dev.spiritstudios.snapper.gui.widget; + +import dev.spiritstudios.snapper.mixin.accessor.AbstractSliderButtonAccessor; +import net.minecraft.client.OptionInstance; +import net.minecraft.client.gui.components.AbstractSliderButton; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.DoubleFunction; +import java.util.function.Function; +import java.util.function.ToDoubleFunction; + +public class ConfigSliderWidget extends AbstractSliderButton { + private final DoubleFunction fromSliderValue; + private final ToDoubleFunction toSliderValue; + + private final Function messageSupplier; + private final Function tooltipSupplier; + private final Consumer onValueChanged; + + private final Component title; + + private T convertedValue; + + public ConfigSliderWidget( + int x, int y, + int width, int height, + Component message, T value, + DoubleFunction fromSliderValue, ToDoubleFunction toSliderValue, + Function messageSupplier, + Function tooltipSupplier, + Consumer onValueChanged + ) { + super(x, y, width, height, message, toSliderValue.applyAsDouble(value)); + this.title = message; + this.fromSliderValue = fromSliderValue; + this.toSliderValue = toSliderValue; + this.messageSupplier = messageSupplier; + this.tooltipSupplier = tooltipSupplier; + this.onValueChanged = onValueChanged; + this.convertedValue = value; + this.updateMessage(); + } + + @Override + protected void updateMessage() { + T value = fromSliderValue.apply(this.value); + this.setMessage(CommonComponents.optionNameValue(title, messageSupplier.apply(value))); + this.setTooltip(tooltipSupplier.apply(value)); + } + + @Override + protected void applyValue() { + onValueChanged.accept(fromSliderValue.apply(this.value)); + } + + @Override + public void onRelease(MouseButtonEvent event) { + super.onRelease(event); + + this.convertedValue = fromSliderValue.apply(value); + + if (this.value != toSliderValue.applyAsDouble(convertedValue)) { + this.value = toSliderValue.applyAsDouble(convertedValue); + this.updateMessage(); + } + } +} diff --git a/src/client/java/dev/spiritstudios/snapper/gui/widget/FolderSelectWidget.java b/src/client/java/dev/spiritstudios/snapper/gui/widget/FolderSelectWidget.java index 0046edd..624c0b7 100644 --- a/src/client/java/dev/spiritstudios/snapper/gui/widget/FolderSelectWidget.java +++ b/src/client/java/dev/spiritstudios/snapper/gui/widget/FolderSelectWidget.java @@ -2,24 +2,20 @@ import dev.spiritstudios.snapper.Snapper; import dev.spiritstudios.snapper.gui.overlay.ExternalDialogOverlay; -import dev.spiritstudios.snapper.util.config.DirectoryConfigUtil; -import dev.spiritstudios.specter.api.config.Value; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.Drawable; -import net.minecraft.client.gui.ParentElement; -import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; -import net.minecraft.client.gui.tooltip.Tooltip; -import net.minecraft.client.gui.widget.ClickableWidget; -import net.minecraft.client.gui.widget.ContainerWidget; -import net.minecraft.client.gui.widget.TextFieldWidget; -import net.minecraft.client.gui.widget.TextIconButtonWidget; -import net.minecraft.screen.ScreenTexts; -import net.minecraft.text.Text; -import net.minecraft.util.Colors; -import net.minecraft.util.Formatting; -import net.minecraft.util.Identifier; +import dev.spiritstudios.snapper.util.DirectoryConfigUtil; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.*; +import net.minecraft.client.gui.components.events.ContainerEventHandler; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.CommonColors; import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; import java.nio.file.Files; import java.nio.file.InvalidPathException; @@ -27,187 +23,209 @@ import java.util.List; import java.util.function.Consumer; -public class FolderSelectWidget extends ContainerWidget implements ParentElement { - private static final Identifier FOLDER_ICON = Snapper.id("screenshots/folder"); - private static final Identifier RESET_ICON = Snapper.id("screenshots/reset"); - - private static final int BUTTON_WIDTH = 25; // Includes padding - - private final Value value; - - private final TextFieldWidget textInput; - private final TextIconButtonWidget fileDialogButton; - private final TextIconButtonWidget resetButton; - - public FolderSelectWidget(int x, int y, int width, int height, Value value, String placeholderKey) { - super(x, y, width, height, ScreenTexts.EMPTY); - this.value = value; - this.active = false; - - MinecraftClient client = MinecraftClient.getInstance(); - - this.textInput = new TextFieldWidget( - client.textRenderer, - BUTTON_WIDTH * 2, 0, - width - (BUTTON_WIDTH * 2), - 20, - Text.of(value.get().toString()) - ); - - this.textInput.setPlaceholder(Text.translatableWithFallback(placeholderKey, "").formatted(Formatting.DARK_GRAY)); - this.textInput.setMaxLength(4096); // Unix maximum path length, shorter on windows (I think it may have been 240) - this.textInput.setText(value.get().toString()); - this.textInput.setChangedListener(content -> { - Path path; - - try { - path = Path.of(content); - } catch (InvalidPathException ignored) { - path = null; - } - - if (path == null || !Files.exists(path) || !Files.isDirectory(path)) { - this.textInput.setEditableColor(Colors.RED); - } else { - this.textInput.setEditableColor(Colors.WHITE); - value.set(path); - } - }); - this.textInput.setTooltip(Tooltip.of(Text.translatable("config.snapper.snapper.customScreenshotFolder.input"))); - - this.fileDialogButton = TextIconButtonWidget.builder( - Text.translatable("config.snapper.snapper.customScreenshotFolder.select"), - button -> { - ExternalDialogOverlay overlay = new ExternalDialogOverlay(); - client.setOverlay(overlay); - - DirectoryConfigUtil.openFolderSelect( - Text.translatable("prompt.snapper.folder_select") - .getString() - ) - .thenAccept(path -> { - valueFromSelectDialog(path.orElse(null)); - client.submit(overlay::close).join(); - }); - }, - true - ) - .width(20) - .texture(FOLDER_ICON, 15, 15) - .build(); - this.fileDialogButton.setTooltip(Tooltip.of(Text.translatable("config.snapper.snapper.customScreenshotFolder.select"))); - - this.resetButton = TextIconButtonWidget.builder( - Text.translatable("config.snapper.snapper.customScreenshotFolder.reset"), - button -> { - value.reset(); - textInput.setText(value.get().toString()); - }, - true - ).width(20).texture(RESET_ICON, 15, 15).build(); - this.resetButton.setTooltip(Tooltip.of(Text.translatable("config.snapper.snapper.customScreenshotFolder.reset"))); - resetButton.setX(BUTTON_WIDTH); - } - - @Override - public List children() { - return List.of( - this.fileDialogButton, this.resetButton, this.textInput - ); - } - - @Override - public void setX(int x) { - super.setX(x); - - fileDialogButton.setX(x); - resetButton.setX(x + BUTTON_WIDTH); - textInput.setX(x + (BUTTON_WIDTH * 2)); - } - - @Override - public void setY(int y) { - super.setY(y); - - fileDialogButton.setY(y); - resetButton.setY(y); - textInput.setY(y); - } - - @Override - public void setWidth(int width) { - super.setWidth(width); - - textInput.setWidth(width - (BUTTON_WIDTH * 2)); - } - - private void valueFromSelectDialog(@Nullable Path value) { - if (value == null) { - return; - } - - if (Files.exists(value)) { - this.value.set(value); - this.textInput.setText(this.value.get().toString()); - } - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - int clicksRan = 0; - - for (ClickableWidget child : this.children()) { - if (child.isHovered()) { - clicksRan += 1; - this.playDownSound(MinecraftClient.getInstance().getSoundManager()); - - if (child == textInput) { - textInput.setFocused(true); - this.setFocused(textInput); - textInput.setFocusUnlocked(true); - } - - child.onClick(mouseX, mouseY); - } - } - - return clicksRan == 1; - } - - @Override - protected void renderWidget(DrawContext context, int mouseX, int mouseY, float deltaTicks) { - for (Drawable drawable : this.children()) { - drawable.render(context, mouseX, mouseY, deltaTicks); - } - } - - @Override - protected void appendClickableNarrations(NarrationMessageBuilder builder) { - fileDialogButton.appendClickableNarrations(builder); - resetButton.appendClickableNarrations(builder); - textInput.appendClickableNarrations(builder); - } - - @Override - public void forEachChild(Consumer consumer) { - this.children().forEach(consumer); - } - - @Override - protected int getContentsHeightWithPadding() { - return 20; - } - - @Override - protected double getDeltaYPerScroll() { - return 20 / 2f; - } - - @Override - public boolean isMouseOver(double mouseX, double mouseY) { - this.active = true; - var hovered = super.isMouseOver(mouseX, mouseY); - this.active = false; - return hovered; - } +public class FolderSelectWidget extends AbstractContainerWidget implements ContainerEventHandler { + private static final ResourceLocation FOLDER_ICON = Snapper.id("screenshots/folder"); + private static final ResourceLocation RESET_ICON = Snapper.id("screenshots/reset"); + + private static final int BUTTON_WIDTH = 25; // Includes padding + + private final PathFunctions value; + + private final EditBox textInput; + private final SpriteIconButton fileDialogButton; + private final SpriteIconButton resetButton; + + public FolderSelectWidget(int x, int y, int width, int height, PathFunctions pathFunctions, String placeholderKey) { + super(x, y, width, height, CommonComponents.EMPTY); + this.value = pathFunctions; + this.active = false; + + Minecraft client = Minecraft.getInstance(); + + this.textInput = new EditBox( + client.font, + BUTTON_WIDTH * 2, 0, + width - (BUTTON_WIDTH * 2), + 20, + Component.literal(pathFunctions.get().toString()) + ); + + this.textInput.setHint(Component.translatableWithFallback(placeholderKey, "").withStyle(ChatFormatting.DARK_GRAY)); + this.textInput.setMaxLength(4096); // Unix maximum path length, shorter on windows (I think it may have been 240) + this.textInput.setValue(pathFunctions.get().toString()); + this.textInput.setResponder(content -> { + Path path; + + try { + path = Path.of(content); + } catch (InvalidPathException ignored) { + path = null; + } + + if (path == null || !Files.exists(path) || !Files.isDirectory(path)) { + this.textInput.setTextColor(CommonColors.RED); + } else { + this.textInput.setTextColor(CommonColors.WHITE); + pathFunctions.set(path); + } + }); + this.textInput.setTooltip(Tooltip.create(Component.translatable("config.snapper.customScreenshotFolder.input"))); + this.textInput.moveCursorToStart(false); + + this.fileDialogButton = SpriteIconButton.builder( + Component.translatable("config.snapper.customScreenshotFolder.select"), + button -> { + ExternalDialogOverlay overlay = new ExternalDialogOverlay(); + client.setOverlay(overlay); + + DirectoryConfigUtil.openFolderSelect( + Component.translatable("prompt.snapper.folder_select") + .getString() + ) + .thenAccept(path -> { + valueFromSelectDialog(path.orElse(null)); + client.submit(overlay::close).join(); + }); + }, + true + ) + .width(20) + .sprite(FOLDER_ICON, 15, 15) + .build(); + this.fileDialogButton.setTooltip(Tooltip.create(Component.translatable("config.snapper.customScreenshotFolder.select"))); + + this.resetButton = SpriteIconButton.builder( + Component.translatable("config.snapper.customScreenshotFolder.reset"), + button -> { + value.reset(); + textInput.setValue(value.get().toString()); + }, + true + ).width(20).sprite(RESET_ICON, 15, 15).build(); + this.resetButton.setTooltip(Tooltip.create(Component.translatable("config.snapper.customScreenshotFolder.reset"))); + resetButton.setX(BUTTON_WIDTH); + } + + @Override + public @NonNull List children() { + return List.of( + this.fileDialogButton, this.resetButton, this.textInput + ); + } + + public void setActive(boolean value) { + this.active = value; + + textInput.setEditable(value); + textInput.active = value; + if (textInput.isFocused() && !value) { + textInput.setFocused(false); + } + + fileDialogButton.active = value; + resetButton.active = value; + } + + @Override + public void setX(int x) { + super.setX(x); + + fileDialogButton.setX(x); + resetButton.setX(x + BUTTON_WIDTH); + textInput.setX(x + (BUTTON_WIDTH * 2)); + } + + @Override + public void setY(int y) { + super.setY(y); + + fileDialogButton.setY(y); + resetButton.setY(y); + textInput.setY(y); + } + + @Override + public void setWidth(int width) { + super.setWidth(width); + + textInput.setWidth(width - (BUTTON_WIDTH * 2)); + } + + private void valueFromSelectDialog(@Nullable Path value) { + if (value == null) { + return; + } + + if (Files.exists(value)) { + this.value.set(value); + this.textInput.setValue(this.value.get().toString()); + } + } + + @Override + public boolean mouseClicked(MouseButtonEvent click, boolean doubled) { + int clicksRan = 0; + + for (AbstractWidget child : this.children()) { + if (child.isHovered() && child.isActive()) { + clicksRan += 1; + this.playDownSound(Minecraft.getInstance().getSoundManager()); + + if (child == textInput) { + textInput.setFocused(true); + this.setFocused(textInput); + textInput.setFocused(true); + } + + child.onClick(click, doubled); + } + } + + return clicksRan == 1; + } + + @Override + protected void renderWidget(GuiGraphics context, int mouseX, int mouseY, float deltaTicks) { + for (Renderable drawable : this.children()) { + drawable.render(context, mouseX, mouseY, deltaTicks); + } + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput builder) { + fileDialogButton.updateWidgetNarration(builder); + resetButton.updateWidgetNarration(builder); + textInput.updateWidgetNarration(builder); + } + + @Override + public void visitWidgets(Consumer consumer) { + this.children().forEach(consumer); + } + + @Override + protected int contentHeight() { + return 20; + } + + @Override + protected double scrollRate() { + return 20 / 2f; + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + this.active = true; + var hovered = super.isMouseOver(mouseX, mouseY); + this.active = false; + return hovered; + } + + public static abstract class PathFunctions { + public abstract Path get(); + + public abstract void set(Path path); + + public abstract void reset(); + } } diff --git a/src/client/java/dev/spiritstudios/snapper/gui/widget/ScreenshotGridWidget.java b/src/client/java/dev/spiritstudios/snapper/gui/widget/ScreenshotGridWidget.java new file mode 100644 index 0000000..24b5909 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/gui/widget/ScreenshotGridWidget.java @@ -0,0 +1,214 @@ +package dev.spiritstudios.snapper.gui.widget; + +import dev.spiritstudios.snapper.util.ScreenshotTexture; +import dev.spiritstudios.snapper.util.SnapperUtil; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.navigation.ScreenDirection; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.util.CommonColors; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Predicate; + +public class ScreenshotGridWidget extends ScreenshotsWidget { + public static final int GRID_ENTRY_WIDTH = 144; + + public ScreenshotGridWidget( + Minecraft client, + int width, int height, + int y, + @Nullable ScreenshotsWidget previous, + Screen parent + ) { + super(client, width, height, y, 81, previous, parent); + } + + private int getColumnCount() { + if (minecraft.screen != null) { + int width = minecraft.screen.width; + + if (width < 480) { + return 2; + } + if (width < 720) { + return 3; + } + return 4; + } + return 3; + } + + @Override + public int getRowWidth() { + return GRID_ENTRY_WIDTH * getColumnCount() + 4 * (getColumnCount() - 1); + } + + @Override + public int getRowTop(int index) { + return super.getRowTop(index / getColumnCount()); + } + + public int getRows() { + return (getItemCount() / getColumnCount()) + (getItemCount() % getColumnCount() > 0 ? 1 : 0); + } + + @Override + protected int contentHeight() { + return this.getRows() * (this.defaultEntryHeight - 4) + 4; + } + + @Override + protected @Nullable Entry nextEntry(ScreenDirection direction, Predicate predicate, @Nullable Entry selected) { + int offset = switch (direction) { + case LEFT -> -1; + case RIGHT -> 1; + case UP -> -getColumnCount(); + case DOWN -> getColumnCount(); + }; + + if (getItemCount() > 0) { + int entryIndex; + if (selected == null) { + entryIndex = offset > 0 ? 0 : getItemCount() - 1; + } else { + entryIndex = this.children().indexOf(selected) + offset; + } + + for (int i = entryIndex; i >= 0 && i < this.children().size(); i += offset) { + Entry entry = this.children().get(i); + if (predicate.test(entry)) { + return entry; + } + } + } + + return null; + } + + @Override + public void repositionEntries() { + int entryCount = this.getItemCount(); + + int rowTop = this.getY() + 2 - (int) this.scrollAmount(); + + int rowLeft = this.getRowLeft(); + int entryHeight = this.defaultEntryHeight - (2 * 2); + int entryWidth = GRID_ENTRY_WIDTH; + + for (int index = 0; index < entryCount; index++) { + int colIndex = index % getColumnCount(); + int leftOffset = colIndex * entryWidth; + + ScreenshotListWidget.Entry entry = this.children().get(index); + entry.setY(rowTop); + entry.setX(rowLeft + leftOffset); + entry.setWidth(entryWidth); + entry.setHeight(entryHeight); + + if (colIndex == getColumnCount() - 1) { + rowTop += entry.getHeight(); + } + } + } + + @Override + protected ScreenshotEntry createEntry(ScreenshotTexture icon) { + return new GridScreenshotEntry(icon); + } + + private class GridScreenshotEntry extends ScreenshotEntry { + public GridScreenshotEntry(ScreenshotTexture icon) { + super(icon); + } + + private boolean safeIsSelected(Entry entry) { + @Nullable Entry nullableSelected = getSelected(); + return (nullableSelected != null && nullableSelected.equals(entry)); + } + + @Override + public void renderContent(GuiGraphics graphics, int mouseX, int mouseY, boolean isHovering, float partialTick) { + int centreX = getContentX() + getContentWidth() / 2; + int centreY = getContentY() + getContentHeight() / 2; + + clickThroughHovered = SnapperUtil.inBoundingBox(centreX - 16, centreY - 16, 32, 32, mouseX, mouseY); + + if (this.icon.loaded()) { + graphics.blit( + RenderPipelines.GUI_TEXTURED, + this.icon.getTextureId(), + getContentX(), getContentY(), + 0, 0, + getContentWidth(), getContentHeight(), + icon.getWidth(), icon.getHeight(), + icon.getWidth(), icon.getHeight() + ); + } + + if (minecraft.options.touchscreen().get() || (isHovering && mouseX < getX() + getWidth()) || safeIsSelected(this)) { + graphics.blit( + RenderPipelines.GUI_TEXTURED, + GRID_SELECTION_BACKGROUND_TEXTURE, + getContentX(), getContentY(), + 0, 0, + getContentWidth(), getContentHeight(), + 16, 16 + ); + + + graphics.blitSprite( + RenderPipelines.GUI_TEXTURED, + clickThroughHovered && icon.loaded() ? + ScreenshotsWidget.VIEW_HIGHLIGHTED_SPRITE : ScreenshotsWidget.VIEW_SPRITE, + centreX - 16, + centreY - 16, + 32, 32 + ); + + graphics.drawString( + minecraft.font, + SnapperUtil.clipText(minecraft.font, fileName, getContentWidth() - 5), + getContentX() + 5, + getContentY() + 6, + CommonColors.WHITE, + true + ); + + graphics.drawString( + minecraft.font, + Component.translatable("text.snapper.created"), + getContentX() + 5, + getContentY() + getContentHeight() - 22, + CommonColors.LIGHT_GRAY, + true + ); + + graphics.drawString( + minecraft.font, + SnapperUtil.clipText(minecraft.font, creation, getContentWidth() - 5), + getContentX() + 5, + getContentY() + getContentHeight() - 12, + CommonColors.LIGHT_GRAY, + true + ); + } + } + + @Override + public boolean mouseClicked(MouseButtonEvent click, boolean doubled) { + setEntrySelected(this); + + if (!clickThroughHovered && Util.getMillis() - this.time >= 250L) { + this.time = Util.getMillis(); + return super.mouseClicked(click, doubled); + } + + return click(); + } + } +} diff --git a/src/client/java/dev/spiritstudios/snapper/gui/widget/ScreenshotListWidget.java b/src/client/java/dev/spiritstudios/snapper/gui/widget/ScreenshotListWidget.java index 99b70ca..62e241c 100644 --- a/src/client/java/dev/spiritstudios/snapper/gui/widget/ScreenshotListWidget.java +++ b/src/client/java/dev/spiritstudios/snapper/gui/widget/ScreenshotListWidget.java @@ -1,589 +1,112 @@ package dev.spiritstudios.snapper.gui.widget; -import dev.spiritstudios.snapper.Snapper; -import dev.spiritstudios.snapper.SnapperConfig; -import dev.spiritstudios.snapper.gui.screen.ScreenshotScreen; -import dev.spiritstudios.snapper.gui.screen.ScreenshotViewerScreen; -import dev.spiritstudios.snapper.mixin.accessor.EntryListWidgetAccessor; -import dev.spiritstudios.snapper.util.SafeFiles; -import dev.spiritstudios.snapper.util.ScreenshotActions; -import dev.spiritstudios.snapper.util.DynamicTexture; +import dev.spiritstudios.snapper.util.ScreenshotTexture; import dev.spiritstudios.snapper.util.SnapperUtil; -import dev.spiritstudios.specter.api.core.exception.UnreachableException; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gl.RenderPipelines; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.navigation.NavigationDirection; -import net.minecraft.client.gui.screen.LoadingDisplay; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.AlwaysSelectedEntryListWidget; -import net.minecraft.client.input.KeyCodes; -import net.minecraft.text.Text; -import net.minecraft.util.Colors; -import net.minecraft.util.Identifier; -import net.minecraft.util.StringHelper; -import net.minecraft.util.Util; -import net.minecraft.util.math.MathHelper; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.util.CommonColors; +import net.minecraft.util.StringUtil; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.FileTime; import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.function.Predicate; - -public class ScreenshotListWidget extends AlwaysSelectedEntryListWidget { - private static final Identifier VIEW_SPRITE = Snapper.id("screenshots/view"); - private static final Identifier VIEW_HIGHLIGHTED_SPRITE = Snapper.id("screenshots/view_highlighted"); - - private static final Identifier GRID_SELECTION_BACKGROUND_TEXTURE = Snapper.id("textures/gui/grid_selection_background.png"); - - private final Screen parent; - - public final CompletableFuture> loadFuture; - - public static final int GRID_ENTRY_WIDTH = 144; - - private final int gridItemHeight = 81; - private final int listItemHeight = 36; - private boolean showGrid = false; +public class ScreenshotListWidget extends ScreenshotsWidget { public ScreenshotListWidget( - MinecraftClient client, + Minecraft client, int width, int height, - int y, int itemHeight, - @Nullable ScreenshotListWidget previous, + int y, + @Nullable ScreenshotsWidget previous, Screen parent ) { - super(client, width, height, y, itemHeight); - - this.parent = parent; - this.addEntry(new LoadingEntry(client)); - - this.loadFuture = previous != null ? previous.loadFuture : load(client); - - this.loadFuture.thenAccept(entries -> { - this.clearEntries(); - entries.forEach(this::addEntry); - - if (entries.isEmpty()) { - this.addEntry(new EmptyEntry(client)); - } - }); - - this.showGrid = SnapperConfig.INSTANCE.viewMode.get().equals(ScreenshotScreen.ViewMode.GRID); - - ((EntryListWidgetAccessor) this).setItemHeight(this.showGrid ? this.gridItemHeight : this.listItemHeight); - } - - @Override - protected void clearEntries() { - this.children().forEach(Entry::close); - super.clearEntries(); - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (KeyCodes.isToggle(keyCode)) { - Entry entry = this.getSelectedOrNull(); - if (entry instanceof ScreenshotEntry screenshotEntry) return screenshotEntry.click(); - } - - return super.keyPressed(keyCode, scanCode, modifiers); - } - - public CompletableFuture> load(MinecraftClient client) { - return CompletableFuture.supplyAsync(() -> { - List screenshots = ScreenshotActions.getScreenshots(); - - return screenshots.parallelStream() - .flatMap(path -> DynamicTexture.createScreenshot(client.getTextureManager(), path).stream()) - .peek(screenshotImage -> screenshotImage.load() - .exceptionally(throwable -> { - Snapper.LOGGER.error("An error occurred while loading the screenshot list", throwable); - return null; - })) - .map(image -> new ScreenshotEntry(image, client, parent, screenshots)) - .sorted(Comparator.comparingLong(ScreenshotEntry::lastModified).reversed()) - .toList(); - }) - .exceptionally(throwable -> { - Snapper.LOGGER.error("An error occurred while loading the screenshot list", throwable); - return Collections.emptyList(); - }); - } - - private void setEntrySelected(@Nullable ScreenshotEntry entry) { - super.setSelected(entry); - if (this.parent instanceof ScreenshotScreen screenshotScreen) { - screenshotScreen.imageSelected(entry); - } - } - - private int getColumnCount() { - if (client.currentScreen != null) { - int width = client.currentScreen.width; - - if (width < 480) { - return 2; - } - if (width < 720) { - return 3; - } - return 4; - } - return 3; - } - - @Override - public int getRowWidth() { - return showGrid ? GRID_ENTRY_WIDTH * getColumnCount() + 4 * (getColumnCount() - 1) : 220; + super(client, width, height, y, 36, previous, parent); } @Override - protected void renderList(DrawContext context, int mouseX, int mouseY, float delta) { - if (showGrid) { - int rowLeft = this.getRowLeft(); - int rowWidth = this.getRowWidth(); - int entryHeight = this.itemHeight - 4; - int entryWidth = GRID_ENTRY_WIDTH; - int entryCount = this.getEntryCount(); - int spacing = (rowWidth - (getColumnCount() * entryWidth)) / (getColumnCount() - 1); - - for (int index = 0; index < entryCount; index++) { - int rowTop = this.getRowTop(index); - int rowBottom = this.getRowBottom(index); - int colIndex = index % getColumnCount(); - int leftOffset = colIndex * (entryWidth + spacing); - - if (rowBottom >= this.getY() && rowTop <= this.getBottom()) { - this.renderEntry(context, mouseX, mouseY, delta, index, rowLeft + leftOffset, rowTop, entryWidth, entryHeight); - } - } - } else { - super.renderList(context, mouseX, mouseY, delta); + public void repositionEntries() { + super.repositionEntries(); + for (var entry : this.children()) { + entry.setHeight(defaultEntryHeight); } } @Override - public int getRowTop(int index) { - return super.getRowTop(showGrid ? index / getColumnCount() : index); - } - - @Override - protected void drawSelectionHighlight(DrawContext context, int y, int entryWidth, int entryHeight, int borderColor, int fillColor) { + protected void renderSelection(GuiGraphics context, Entry entry, int color) { // let elements handle it } @Override - public int getMaxScrollY() { - int totalRows = (getEntryCount() / getColumnCount()) + (getEntryCount() % getColumnCount() > 0 ? 1 : 0); - return showGrid ? Math.max(0, totalRows * itemHeight - this.height + 4) : super.getMaxScrollY(); - } - - @Override - protected int getContentsHeightWithPadding() { - if (!this.showGrid) return super.getContentsHeightWithPadding(); - int totalRows = (getEntryCount() / getColumnCount()) + (getEntryCount() % getColumnCount() > 0 ? 1 : 0); - return totalRows * this.itemHeight + this.headerHeight + 4; - } - - public void toggleGrid() { - this.showGrid = !this.showGrid; - ((EntryListWidgetAccessor) this).setItemHeight(this.showGrid ? this.gridItemHeight : this.listItemHeight); - for (var entry : this.children()) if (entry instanceof ScreenshotEntry sc) sc.setShowGrid(this.showGrid); - - SnapperConfig.INSTANCE.viewMode.set(this.showGrid ? ScreenshotScreen.ViewMode.GRID : ScreenshotScreen.ViewMode.LIST); - } - - @Override - protected @Nullable Entry getEntryAtPosition(double x, double y) { - if (!showGrid) return super.getEntryAtPosition(x, y); - - int rowWidth = this.getRowWidth(); - int relX = MathHelper.floor(x - this.getRowLeft()); - int relY = MathHelper.floor(y - (double) this.getY()) - this.headerHeight; - - if (relX < 0 || relX > rowWidth || relY < 0 || relY > getBottom()) return null; - - int rowIndex = (relY + (int) this.getScrollY()) / this.itemHeight; - int colIndex = MathHelper.floor(((float) relX / (float) rowWidth) * (float) getColumnCount()); - int entryIndex = rowIndex * getColumnCount() + colIndex; - - return entryIndex >= 0 && entryIndex < getEntryCount() ? getEntry(entryIndex) : null; - } - - public abstract static class Entry extends AlwaysSelectedEntryListWidget.Entry implements AutoCloseable { - public void close() { - } - } - - @Override - protected @Nullable Entry getNeighboringEntry(NavigationDirection direction, Predicate predicate, @Nullable Entry selected) { - if (!showGrid) return super.getNeighboringEntry(direction, predicate, selected); - int offset = switch (direction) { - case LEFT -> -1; - case RIGHT -> 1; - case UP -> -getColumnCount(); - case DOWN -> getColumnCount(); - }; - - if (getEntryCount() > 0) { - int entryIndex; - if (selected == null) { - entryIndex = offset > 0 ? 0 : getEntryCount() - 1; - } else { - entryIndex = this.children().indexOf(selected) + offset; - } - - for (int k = entryIndex; k >= 0 && k < this.children().size(); k += offset) { - Entry entry = getEntry(k); - if (predicate.test(entry)) { - return entry; - } - } - } - - return null; - } - - public static class LoadingEntry extends Entry implements AutoCloseable { - private static final Text LOADING_LIST_TEXT = Text.translatable("text.snapper.loading"); - private final MinecraftClient client; - - public LoadingEntry(MinecraftClient client) { - this.client = client; - } - - @Override - public Text getNarration() { - return LOADING_LIST_TEXT; - } - - @Override - public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { - if (this.client.currentScreen == null) throw new UnreachableException(); - - context.drawText( - this.client.textRenderer, - LOADING_LIST_TEXT, - (this.client.currentScreen.width - this.client.textRenderer.getWidth(LOADING_LIST_TEXT)) / 2, - y + (entryHeight - 9) / 2, - Colors.WHITE, - false - ); - - String loadString = LoadingDisplay.get(Util.getMeasuringTimeMs()); - - context.drawText( - this.client.textRenderer, - loadString, - (this.client.currentScreen.width - this.client.textRenderer.getWidth(loadString)) / 2, - y + (entryHeight - 9) / 2 + 9, - Colors.GRAY, - false - ); - } - } - - public static class EmptyEntry extends Entry implements AutoCloseable { - private static final Text EMPTY_LIST_TEXT = Text.translatable("text.snapper.empty"); - private static final Text EMPTY_CUSTOM_LIST_TEXT = Text.translatable("text.snapper.empty.custom"); - private final MinecraftClient client; - - public EmptyEntry(MinecraftClient client) { - this.client = client; - } - - @Override - public Text getNarration() { - return EMPTY_LIST_TEXT; - } - - @Override - public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { - if (this.client.currentScreen == null) throw new UnreachableException(); - - context.drawText( - this.client.textRenderer, - EMPTY_LIST_TEXT, - (this.client.currentScreen.width - this.client.textRenderer.getWidth(EMPTY_LIST_TEXT)) / 2, - y + entryHeight / 2, - Colors.WHITE, - false - ); - - context.drawText( - this.client.textRenderer, - EMPTY_CUSTOM_LIST_TEXT, - (this.client.currentScreen.width - this.client.textRenderer.getWidth(EMPTY_CUSTOM_LIST_TEXT)) / 2, - y + entryHeight / 2 + 10, - Colors.WHITE, - false - ); - } + protected ScreenshotEntry createEntry(ScreenshotTexture icon) { + return new ListScreenshotEntry(icon); } - public class ScreenshotEntry extends Entry implements AutoCloseable { - public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter - .ofLocalizedDateTime(FormatStyle.SHORT) - .withZone(ZoneId.systemDefault()); - - public final FileTime lastModified; - private final MinecraftClient client; - public final DynamicTexture icon; - public final String iconFileName; - public final Screen screenParent; - private long time; - private boolean showGrid; - private final List screenshots; - private boolean clickThroughHovered = false; - - public ScreenshotEntry(DynamicTexture icon, MinecraftClient client, Screen parent, List screenshots) { - this.showGrid = ScreenshotListWidget.this.showGrid; - this.client = client; - this.screenParent = parent; - this.icon = icon; - this.iconFileName = icon.getPath().getFileName().toString(); - this.lastModified = SafeFiles.getLastModifiedTime(icon.getPath()).orElse(FileTime.fromMillis(0L)); - this.screenshots = screenshots; - } - - public void setShowGrid(boolean showGrid) { - this.showGrid = showGrid; + private class ListScreenshotEntry extends ScreenshotEntry { + public ListScreenshotEntry(ScreenshotTexture icon) { + super(icon); } @Override - public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { - if (this.showGrid) { - renderGrid(context, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickDelta); - return; - } - renderList(context, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickDelta); - } - - public void renderList(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { - String fileName = this.iconFileName; - String creationString = "undefined"; - - long creationTime = 0; - try { - creationTime = Files.readAttributes(icon.getPath(), BasicFileAttributes.class).creationTime().toMillis(); - } catch (IOException e) { - client.setScreen(new ScreenshotScreen(screenParent)); - } - - if (creationTime != -1L) - creationString = Text.translatable("text.snapper.created").getString() + " " + DATE_FORMAT.format(Instant.ofEpochMilli(creationTime)); - - if (StringHelper.isEmpty(fileName)) - fileName = Text.translatable("text.snapper.generic") + " " + (index + 1); - - context.drawText( - this.client.textRenderer, - truncateFileName(fileName, entryWidth - 32 - 6, 29), - x + 32 + 3, y + 1, - Colors.WHITE, + public void renderContent(GuiGraphics graphics, int mouseX, int mouseY, boolean isHovering, float partialTick) { + graphics.drawString( + minecraft.font, + SnapperUtil.clipText(minecraft.font, fileName, getContentWidth() - 32 - 6), + getContentX() + 32 + 3, getContentY() + 1, + CommonColors.WHITE, false ); - context.drawText( - this.client.textRenderer, - creationString, - x + 35, y + 12, - Colors.GRAY, + graphics.drawString( + minecraft.font, + creation, + getContentX() + 35, getContentY() + 12, + CommonColors.GRAY, false ); if (icon.loaded()) { - //noinspection SuspiciousNameCombination - context.drawTexture( + graphics.blit( RenderPipelines.GUI_TEXTURED, this.icon.getTextureId(), - x, y, + getContentX(), getContentY(), (icon.getHeight()) / 3.0f + 32, 0, - entryHeight, entryHeight, + getContentHeight(), getContentHeight(), icon.getHeight(), icon.getHeight(), icon.getWidth(), icon.getHeight() ); } - if (this.client.options.getTouchscreen().getValue() || hovered) { - context.fill(x, y, x + 32, y + 32, 0xA0909090); - context.drawGuiTexture( + if (minecraft.options.touchscreen().get() || isHovering) { + graphics.fill(getContentX(), getContentY(), getContentX() + 32, getContentY() + 32, 0xA0909090); + graphics.blitSprite( RenderPipelines.GUI_TEXTURED, - mouseX - x < 32 && this.icon.loaded() ? - ScreenshotListWidget.VIEW_HIGHLIGHTED_SPRITE : - ScreenshotListWidget.VIEW_SPRITE, - x, y, + mouseX - getContentX() < 32 && this.icon.loaded() ? + ScreenshotsWidget.VIEW_HIGHLIGHTED_SPRITE : + ScreenshotsWidget.VIEW_SPRITE, + getContentX(), getContentY(), 32, 32 ); } } - public void renderGrid(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { - int centreX = x + entryWidth / 2; - int centreY = y + entryHeight / 2; - - clickThroughHovered = SnapperUtil.inBoundingBox(centreX - 16, centreY - 16, 32, 32, mouseX, mouseY); - - if (this.icon.loaded()) { - context.drawTexture( - RenderPipelines.GUI_TEXTURED, - this.icon.getTextureId(), - x, y, - 0, 0, - entryWidth, entryHeight, - icon.getWidth(), icon.getHeight(), - icon.getWidth(), icon.getHeight() - ); - } - - if (this.client.options.getTouchscreen().getValue() || (hovered && mouseX < x + entryWidth) || isSelectedEntry(index)) { - renderMetadata(context, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickDelta); - } - } - - public void renderMetadata(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { - String fileName = this.iconFileName; - - int centreX = x + entryWidth / 2; - int centreY = y + entryHeight / 2; - - if (StringHelper.isEmpty(fileName)) - fileName = Text.translatable("text.snapper.generic") + " " + (index + 1); - - String creationString = "undefined"; - long creationTime = 0; - try { - creationTime = Files.readAttributes(icon.getPath(), BasicFileAttributes.class).creationTime().toMillis(); - } catch (IOException e) { - client.setScreen(new ScreenshotScreen(screenParent)); - } - - if (creationTime != -1L) - creationString = DATE_FORMAT.format(Instant.ofEpochMilli(creationTime)); - - context.drawTexture( - RenderPipelines.GUI_TEXTURED, - GRID_SELECTION_BACKGROUND_TEXTURE, - x, y, - 0, 0, - entryWidth, entryHeight, - 16, 16 - ); - - - context.drawGuiTexture( - RenderPipelines.GUI_TEXTURED, - clickThroughHovered && icon.loaded() ? - ScreenshotListWidget.VIEW_HIGHLIGHTED_SPRITE : ScreenshotListWidget.VIEW_SPRITE, - centreX - 16, - centreY - 16, - 32, - 32 - ); - - context.drawText( - this.client.textRenderer, - truncateFileName(fileName, entryWidth, 24), - x + 5, - y + 6, - Colors.WHITE, - true - ); - - context.drawText( - this.client.textRenderer, - Text.translatable("text.snapper.created"), - x + 5, - y + entryHeight - 22, - Colors.LIGHT_GRAY, - true - ); - - context.drawText( - this.client.textRenderer, - creationString, - x + 5, - y + entryHeight - 12, - Colors.LIGHT_GRAY, - true - ); - } - - public String truncateFileName(String fileName, int maxWidth, int truncateLength) { - String truncatedName = fileName; - if (this.client.textRenderer.getWidth(truncatedName) > maxWidth) - truncatedName = truncatedName.substring(0, Math.min(fileName.length(), truncateLength)) + "..."; - return truncatedName; - } - - @Override - public void drawBorder(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { - if (isSelectedEntry(index)) { - context.fill(x - 2, y - 2, x + entryWidth + 2, y + entryHeight + 2, -1); - context.fill(x - 1, y - 1, x + entryWidth + 1, y + entryHeight + 1, -16777216); - } - } - - @Override - public void setFocused(boolean focused) { - if (focused) { - setEntrySelected(this); - } - super.setFocused(focused); - } - @Override - public Text getNarration() { - return Text.literal(this.iconFileName); - } + public boolean mouseClicked(MouseButtonEvent click, boolean doubled) { + setEntrySelected(this); - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - ScreenshotListWidget.this.setEntrySelected(this); + boolean clickThrough = click.x() - ScreenshotListWidget.this.getRowLeft() <= 32.0F; - boolean clickThrough = - ( - !this.showGrid && - mouseX - (double) ScreenshotListWidget.this.getRowLeft() <= 32.0 - ) || - ( - this.showGrid && - clickThroughHovered - ); - if (!clickThrough && Util.getMeasuringTimeMs() - this.time >= 250L) { - this.time = Util.getMeasuringTimeMs(); - return super.mouseClicked(mouseX, mouseY, button); + if (!clickThrough && Util.getMillis() - this.time >= 250L) { + this.time = Util.getMillis(); + return super.mouseClicked(click, doubled); } return click(); } - - public boolean click() { - if (this.icon == null) return false; - playClickSound(this.client.getSoundManager()); - this.client.setScreen(new ScreenshotViewerScreen(this.icon, icon.getPath(), this.screenParent, this.screenshots)); - return true; - } - - @Override - public void close() { - this.icon.close(); - } - - public long lastModified() { - return lastModified.toMillis(); - } } } diff --git a/src/client/java/dev/spiritstudios/snapper/gui/widget/ScreenshotsWidget.java b/src/client/java/dev/spiritstudios/snapper/gui/widget/ScreenshotsWidget.java new file mode 100644 index 0000000..5f74db1 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/gui/widget/ScreenshotsWidget.java @@ -0,0 +1,300 @@ +package dev.spiritstudios.snapper.gui.widget; + +import com.mojang.blaze3d.platform.InputConstants; +import dev.spiritstudios.snapper.Snapper; +import dev.spiritstudios.snapper.SnapperConfig; +import dev.spiritstudios.snapper.gui.screen.ScreenshotListScreen; +import dev.spiritstudios.snapper.gui.screen.ScreenshotViewerScreen; +import dev.spiritstudios.snapper.util.SafeFiles; +import dev.spiritstudios.snapper.util.ScreenshotActions; +import dev.spiritstudios.snapper.util.ScreenshotTexture; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.ObjectSelectionList; +import net.minecraft.client.gui.screens.LoadingDotsText; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.CommonColors; +import net.minecraft.util.StringUtil; +import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public abstract class ScreenshotsWidget extends ObjectSelectionList { + protected static final ResourceLocation VIEW_SPRITE = Snapper.id("screenshots/view"); + protected static final ResourceLocation VIEW_HIGHLIGHTED_SPRITE = Snapper.id("screenshots/view_highlighted"); + protected static final ResourceLocation GRID_SELECTION_BACKGROUND_TEXTURE = Snapper.id("textures/gui/grid_selection_background.png"); + + protected final Screen parent; + public final CompletableFuture> loadFuture; + + public static ScreenshotsWidget create( + Minecraft client, + int width, int height, + int y, @Nullable ScreenshotsWidget previous, + Screen parent + ) { + if (SnapperConfig.HOLDER.get().viewMode() == ScreenshotListScreen.ViewMode.GRID) { + return new ScreenshotGridWidget(client, width, height, y, previous, parent); + } else { + return new ScreenshotListWidget(client, width, height, y, previous, parent); + } + } + + public ScreenshotsWidget( + Minecraft client, + int width, int height, + int y, int itemHeight, + @Nullable ScreenshotsWidget previous, + Screen parent + ) { + super(client, width, height, y, itemHeight); + + this.parent = parent; + this.addEntry(new ScreenshotsWidget.LoadingEntry(client)); + + this.loadFuture = previous != null ? previous.loadFuture : load(client); + + this.loadFuture.thenAccept(textures -> { + this.clearEntries(); + + if (textures.isEmpty()) { + this.addEntry(new ScreenshotsWidget.EmptyEntry(client)); + } else { + for (ScreenshotTexture texture : textures) { + addEntry(createEntry(texture)); + } + } + + repositionEntries(); + }).exceptionally(ex -> { + clearEntries(); + // TODO: Error Entry + this.addEntry(new ScreenshotsWidget.EmptyEntry(client)); + + repositionEntries(); + + Snapper.LOGGER.error("Failed to load textures", ex); + + return null; + }); + + repositionEntries(); + } + + @Override + protected void clearEntries() { + this.children().forEach(Entry::close); + super.clearEntries(); + } + + protected void setEntrySelected(@Nullable ScreenshotEntry entry) { + super.setSelected(entry); + if (this.parent instanceof ScreenshotListScreen screenshotScreen) { + screenshotScreen.imageSelected(entry); + } + } + + @Override + public boolean keyPressed(KeyEvent input) { + if (input.key() == InputConstants.KEY_RETURN) { + Entry entry = this.getSelected(); + if (entry instanceof ScreenshotEntry screenshotEntry) return screenshotEntry.click(); + } + + return super.keyPressed(input); + } + + + protected abstract ScreenshotEntry createEntry(ScreenshotTexture icon); + + public CompletableFuture> load(Minecraft client) { + return CompletableFuture.supplyAsync(() -> { + List screenshots = ScreenshotActions.getScreenshots(); + + return screenshots.parallelStream() + .flatMap(path -> ScreenshotTexture.createScreenshot(client.getTextureManager(), path).stream()) + .peek(screenshotImage -> screenshotImage.load() + .exceptionally(throwable -> { + Snapper.LOGGER.error("An error occurred while loading the screenshot list", throwable); + return null; + })) + .sorted(Comparator.comparing(texture -> + SafeFiles.getLastModifiedTime(texture.getPath()).orElse(FileTime.fromMillis(0L))).reversed()) + .toList(); + }) + .exceptionally(throwable -> { + Snapper.LOGGER.error("An error occurred while loading the screenshot list", throwable); + return Collections.emptyList(); + }); + } + + public abstract static class Entry extends ObjectSelectionList.Entry implements AutoCloseable { + public void close() { + } + } + + public static class LoadingEntry extends Entry implements AutoCloseable { + private static final Component LOADING_LIST_TEXT = Component.translatable("text.snapper.loading"); + private final Minecraft client; + + public LoadingEntry(Minecraft client) { + this.client = client; + } + + @Override + public @NonNull Component getNarration() { + return LOADING_LIST_TEXT; + } + + @Override + public void renderContent(GuiGraphics context, int mouseX, int mouseY, boolean isHovering, float partialTick) { + if (this.client.screen == null) throw new IllegalStateException(); + + context.drawString( + this.client.font, + LOADING_LIST_TEXT, + (this.client.screen.width - this.client.font.width(LOADING_LIST_TEXT)) / 2, + getY() + (getHeight() - 9) / 2, + CommonColors.WHITE, + false + ); + + String loadString = LoadingDotsText.get(Util.getMillis()); + + context.drawString( + this.client.font, + loadString, + (this.client.screen.width - this.client.font.width(loadString)) / 2, + getY() + (getHeight() - 9) / 2 + 9, + CommonColors.GRAY, + false + ); + } + } + + public static class EmptyEntry extends Entry implements AutoCloseable { + private static final Component EMPTY_LIST_TEXT = Component.translatable("text.snapper.empty"); + private static final Component EMPTY_CUSTOM_LIST_TEXT = Component.translatable("text.snapper.empty.custom"); + private final Minecraft minecraft; + + public EmptyEntry(Minecraft minecraft) { + this.minecraft = minecraft; + } + + @Override + public @NonNull Component getNarration() { + return EMPTY_LIST_TEXT; + } + + @Override + public void renderContent(GuiGraphics context, int mouseX, int mouseY, boolean isHovering, float partialTick) { + if (this.minecraft.screen == null) throw new IllegalStateException(); + + context.drawString( + this.minecraft.font, + EMPTY_LIST_TEXT, + (this.minecraft.screen.width - this.minecraft.font.width(EMPTY_LIST_TEXT)) / 2, + getY() + getHeight() / 2, + CommonColors.WHITE, + false + ); + + context.drawString( + this.minecraft.font, + EMPTY_CUSTOM_LIST_TEXT, + (this.minecraft.screen.width - this.minecraft.font.width(EMPTY_CUSTOM_LIST_TEXT)) / 2, + getY() + getHeight() / 2 + 10, + CommonColors.WHITE, + false + ); + } + } + + public abstract class ScreenshotEntry extends Entry implements AutoCloseable { + public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter + .ofLocalizedDateTime(FormatStyle.SHORT) + .withZone(ZoneId.systemDefault()); + + public final FileTime lastModified; + public final ScreenshotTexture icon; + public final Screen screenParent; + + protected final Component fileName; + protected final Component creation; + + protected long time; + protected boolean clickThroughHovered = false; + protected final int index; + + public ScreenshotEntry(ScreenshotTexture icon) { + this.screenParent = parent; + this.icon = icon; + this.index = children().indexOf(this); + this.lastModified = SafeFiles.getLastModifiedTime(icon.getPath()).orElse(FileTime.fromMillis(0L)); + + String fileName = icon.getPath().getFileName().toString(); + + this.fileName = StringUtil.isNullOrEmpty(fileName) ? + Component.translatable("text.snapper.generic", this.index + 1) : + Component.literal(fileName); + + Component creation = Component.translatable("text.snapper.unknown"); + + long creationTime = 0; + try { + creationTime = Files.readAttributes(icon.getPath(), BasicFileAttributes.class).creationTime().toMillis(); + } catch (IOException ignored) { + } + + if (creationTime != -1L) creation = Component.literal(DATE_FORMAT.format(Instant.ofEpochMilli(creationTime))); + + this.creation = creation; + } + + @Override + public void setFocused(boolean focused) { + if (focused) { + setEntrySelected(this); + } + super.setFocused(focused); + } + + @Override + public @NonNull Component getNarration() { + return fileName; + } + + public boolean click() { + if (this.icon == null) return false; + playButtonClickSound(minecraft.getSoundManager()); + minecraft.setScreen(new ScreenshotViewerScreen(this.icon, icon.getPath(), this.screenParent, null)); + return true; + } + + @Override + public void close() { + this.icon.close(); + } + + public long lastModified() { + return lastModified.toMillis(); + } + } +} diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/CameraMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/CameraMixin.java index 53b4020..348e132 100644 --- a/src/client/java/dev/spiritstudios/snapper/mixin/CameraMixin.java +++ b/src/client/java/dev/spiritstudios/snapper/mixin/CameraMixin.java @@ -1,9 +1,9 @@ package dev.spiritstudios.snapper.mixin; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.render.Camera; -import net.minecraft.entity.Entity; -import net.minecraft.world.BlockView; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.BlockGetter; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -11,15 +11,15 @@ @Mixin(Camera.class) public abstract class CameraMixin { - @Inject(method = "update", at = @At("HEAD"), cancellable = true) + @Inject(method = "setup", at = @At("HEAD"), cancellable = true) private void blockUpdateDuringPanoramaRender( - BlockView area, - Entity focusedEntity, - boolean thirdPerson, - boolean inverseView, - float tickDelta, + BlockGetter level, + Entity entity, + boolean detached, + boolean thirdPersonReverse, + float partialTick, CallbackInfo ci ) { - if (MinecraftClient.getInstance().gameRenderer.isRenderingPanorama() && thirdPerson) ci.cancel(); + if (Minecraft.getInstance().gameRenderer.isPanoramicMode() && detached) ci.cancel(); } } diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/GameMenuMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/GameMenuMixin.java deleted file mode 100644 index 1f62a16..0000000 --- a/src/client/java/dev/spiritstudios/snapper/mixin/GameMenuMixin.java +++ /dev/null @@ -1,46 +0,0 @@ -package dev.spiritstudios.snapper.mixin; - -import dev.spiritstudios.snapper.Snapper; -import dev.spiritstudios.snapper.SnapperConfig; -import dev.spiritstudios.snapper.gui.screen.ScreenshotScreen; -import net.minecraft.client.gui.screen.GameMenuScreen; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.TextIconButtonWidget; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(GameMenuScreen.class) -public abstract class GameMenuMixin extends Screen { - protected GameMenuMixin(Text title) { - super(title); - } - - @Unique - private static final Identifier SNAPPER_BUTTON_ICON = Snapper.id("screenshots/screenshot"); - - @Inject( - method = "initWidgets", - at = @At("TAIL") - ) - protected void initWidgets(CallbackInfo ci) { - if (SnapperConfig.INSTANCE.showSnapperGameMenu.get()) { - this.addDrawableChild( - TextIconButtonWidget.builder( - Text.translatable("button.snapper.screenshots"), - button -> { - if (this.client == null) - return; - - this.client.setScreen(new ScreenshotScreen(new GameMenuScreen(true))); - }, - true - ).width(20).texture(SNAPPER_BUTTON_ICON, 15, 15).build() - ).setPosition(this.width / 2 - 130, height / 4 + 32); - } - } -} diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/InGameHudMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/InGameHudMixin.java index 63be3b7..62d9477 100644 --- a/src/client/java/dev/spiritstudios/snapper/mixin/InGameHudMixin.java +++ b/src/client/java/dev/spiritstudios/snapper/mixin/InGameHudMixin.java @@ -4,27 +4,26 @@ import com.llamalad7.mixinextras.expression.Expression; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import dev.spiritstudios.snapper.gui.screen.PanoramaViewerScreen; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.hud.InGameHud; -import net.minecraft.client.gui.screen.DownloadingTerrainScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.screens.LevelLoadingScreen; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; -@Mixin(InGameHud.class) +@Mixin(Gui.class) public class InGameHudMixin { - @Shadow @Final - private MinecraftClient client; + private Minecraft minecraft; - @Definition(id = "DownloadingTerrainScreen", type = DownloadingTerrainScreen.class) - @Definition(id = "client", field = "Lnet/minecraft/client/gui/hud/InGameHud;client:Lnet/minecraft/client/MinecraftClient;") - @Definition(id = "currentScreen", field = "Lnet/minecraft/client/MinecraftClient;currentScreen:Lnet/minecraft/client/gui/screen/Screen;") - @Expression("(this.client.currentScreen instanceof DownloadingTerrainScreen)") + @Definition(id = "LevelLoadingScreen", type = LevelLoadingScreen.class) + @Definition(id = "minecraft", field = "Lnet/minecraft/client/gui/Gui;minecraft:Lnet/minecraft/client/Minecraft;") + @Definition(id = "screen", field = "Lnet/minecraft/client/Minecraft;screen:Lnet/minecraft/client/gui/screens/Screen;") + @Expression("(this.minecraft.screen instanceof LevelLoadingScreen)") @ModifyExpressionValue(method = "render", at = @At(value = "MIXINEXTRAS:EXPRESSION")) private boolean cancelRenderingHudInPanoramaScreen(boolean original) { - return original || client.currentScreen instanceof PanoramaViewerScreen; + return original || minecraft.screen instanceof PanoramaViewerScreen; } } diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/KeyboardHandlerMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/KeyboardHandlerMixin.java new file mode 100644 index 0000000..628dfc3 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/mixin/KeyboardHandlerMixin.java @@ -0,0 +1,49 @@ +package dev.spiritstudios.snapper.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.mojang.blaze3d.pipeline.RenderTarget; +import dev.spiritstudios.snapper.SnapperConfig; +import dev.spiritstudios.snapper.SnapperKeybindings; +import dev.spiritstudios.snapper.gui.toast.SnapperToast; +import net.minecraft.client.KeyboardHandler; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +import java.io.File; +import java.util.function.Consumer; + +@Mixin(KeyboardHandler.class) +public abstract class KeyboardHandlerMixin { + @Shadow + @Final + private Minecraft minecraft; + + @WrapOperation(method = "keyPress", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Screenshot;grab(Ljava/io/File;Lcom/mojang/blaze3d/pipeline/RenderTarget;Ljava/util/function/Consumer;)V")) + private void showDebugChat(File gameDirectory, RenderTarget renderTarget, Consumer messageConsumer, Operation original) { + original.call( + gameDirectory, + renderTarget, + (Consumer) message -> { + // Execute on the render thread. + Minecraft.getInstance().execute(() -> { + // Lovely tree of decisions to decide what instructions make sense. <3 Lynn + String inGameDeterminedDescription = minecraft.screen == null ? "toast.snapper.screenshot.created.description" + : "toast.snapper.screenshot.created.description_in_menu"; + String copyDeterminedDescription = SnapperConfig.HOLDER.get().copyTakenScreenshot() ? + "toast.snapper.screenshot.created.description_copy" : inGameDeterminedDescription; + + SnapperToast.push( + SnapperToast.Type.SCREENSHOT, + Component.translatable("toast.snapper.screenshot.created"), + Component.translatable(copyDeterminedDescription, message, SnapperKeybindings.RECENT_SCREENSHOT_KEY.getTranslatedKeyMessage()) + ); + }); + } + ); + } +} diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/KeyboardMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/KeyboardMixin.java deleted file mode 100644 index 62b457a..0000000 --- a/src/client/java/dev/spiritstudios/snapper/mixin/KeyboardMixin.java +++ /dev/null @@ -1,39 +0,0 @@ -package dev.spiritstudios.snapper.mixin; - -import dev.spiritstudios.snapper.SnapperConfig; -import dev.spiritstudios.snapper.SnapperKeybindings; -import dev.spiritstudios.snapper.gui.toast.SnapperToast; -import dev.spiritstudios.snapper.util.SnapperUtil; -import net.minecraft.client.Keyboard; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Text; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Overwrite; -import org.spongepowered.asm.mixin.Shadow; - -@Mixin(Keyboard.class) -public abstract class KeyboardMixin { - @Shadow - @Final - private MinecraftClient client; - - /** - * @author CallMeEcho & WorldWidePixel - * @reason Change message logic to show a toast instead of chat message - */ - @Overwrite - private void method_1464(Text text) { - // Lovely tree of decisions to decide what instructions make sense. <3 Lynn - String inGameDeterminedDescription = client.currentScreen == null ? "toast.snapper.screenshot.created.description" - : "toast.snapper.screenshot.created.description_in_menu"; - String copyDeterminedDescription = SnapperConfig.INSTANCE.copyTakenScreenshot.get() ? - "toast.snapper.screenshot.created.description_copy" : inGameDeterminedDescription; - - SnapperUtil.toast( - SnapperToast.Type.SCREENSHOT, - Text.translatable("toast.snapper.screenshot.created"), - Text.translatable(copyDeterminedDescription, text, SnapperKeybindings.RECENT_SCREENSHOT_KEY.getBoundKeyLocalizedText()) - ); - } -} diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/MinecraftClientMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/MinecraftClientMixin.java deleted file mode 100644 index cec8597..0000000 --- a/src/client/java/dev/spiritstudios/snapper/mixin/MinecraftClientMixin.java +++ /dev/null @@ -1,88 +0,0 @@ -package dev.spiritstudios.snapper.mixin; - -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import com.llamalad7.mixinextras.sugar.Share; -import com.llamalad7.mixinextras.sugar.ref.LocalFloatRef; -import dev.spiritstudios.snapper.SnapperConfig; -import dev.spiritstudios.snapper.mixin.accessor.CameraAccessor; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.RunArgs; -import net.minecraft.client.gl.Framebuffer; -import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.client.option.GameOptions; -import net.minecraft.client.render.GameRenderer; -import net.minecraft.text.Text; -import org.spongepowered.asm.mixin.Final; -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.Constant; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.ModifyConstant; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.io.File; -import java.util.function.Consumer; - -@Mixin(MinecraftClient.class) -public abstract class MinecraftClientMixin { - @Final - @Shadow - public static boolean IS_SYSTEM_MAC; - - @Final - @Shadow - public GameOptions options; - - @Final - @Shadow - public GameRenderer gameRenderer; - - @WrapOperation( - method = "takePanorama", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/util/ScreenshotRecorder;saveScreenshot(Ljava/io/File;Ljava/lang/String;Lnet/minecraft/client/gl/Framebuffer;ILjava/util/function/Consumer;)V") - ) - private void saveScreenshot(File gameDirectory, String fileName, Framebuffer framebuffer, int downscaleFactor, Consumer messageReceiver, Operation original) { - fileName = "panorama/" + fileName; - original.call(gameDirectory, fileName, framebuffer, downscaleFactor, messageReceiver); - } - - @Inject( - method = "", - at = @At("TAIL") - ) - private void init(RunArgs args, CallbackInfo ci) { - if (!IS_SYSTEM_MAC) System.setProperty("java.awt.headless", "false"); - } - - @WrapOperation( - method = "takePanorama", - at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;setYaw(F)V") - ) - private void captureSetYaw(ClientPlayerEntity player, float value, Operation op, @Share("yaw")LocalFloatRef yaw) { - if (!this.options.getPerspective().isFirstPerson()) yaw.set(value); - else op.call(player, value); - } - - @WrapOperation( - method = "takePanorama", - at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;setPitch(F)V") - ) - private void applyThirdPersonCameraRotation(ClientPlayerEntity player, float value, Operation op, @Share("yaw")LocalFloatRef yaw) { - if (!this.options.getPerspective().isFirstPerson()) - ((CameraAccessor) this.gameRenderer.getCamera()).invokeSetRotation(yaw.get(), value); - else op.call(player, value); - } - - @ModifyConstant( - method = "takePanorama", - constant = @Constant(intValue = 4096) - ) - private int configurablePanoramaSize(int original) { - return SnapperConfig.INSTANCE.panoramaDimensions.get().size() * 4; - } - -} diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/MinecraftMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/MinecraftMixin.java new file mode 100644 index 0000000..464e383 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/mixin/MinecraftMixin.java @@ -0,0 +1,85 @@ +package dev.spiritstudios.snapper.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalFloatRef; +import com.mojang.blaze3d.pipeline.RenderTarget; +import dev.spiritstudios.snapper.SnapperConfig; +import dev.spiritstudios.snapper.mixin.accessor.CameraAccessor; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.Options; +import net.minecraft.client.main.GameConfig; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import org.spongepowered.asm.mixin.Final; +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.Constant; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyConstant; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.io.File; +import java.util.function.Consumer; + +@Mixin(Minecraft.class) +public abstract class MinecraftMixin { + @Final + @Shadow + public Options options; + + @Final + @Shadow + public GameRenderer gameRenderer; + + @WrapOperation( + method = "grabPanoramixScreenshot", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/Screenshot;grab(Ljava/io/File;Ljava/lang/String;Lcom/mojang/blaze3d/pipeline/RenderTarget;ILjava/util/function/Consumer;)V") + ) + private void saveScreenshot(File gameDirectory, String fileName, RenderTarget renderTarget, int downscaleFactor, Consumer messageReceiver, Operation original) { + fileName = "panorama/" + fileName; + original.call(gameDirectory, fileName, renderTarget, downscaleFactor, messageReceiver); + } + + @Inject( + method = "", + at = @At("TAIL") + ) + private void init(GameConfig args, CallbackInfo ci) { + if (Util.getPlatform() != Util.OS.OSX) System.setProperty("java.awt.headless", "false"); + } + + @WrapOperation( + method = "grabPanoramixScreenshot", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;setYRot(F)V") + ) + private void captureSetYaw(LocalPlayer player, float value, Operation op, @Share("yaw") LocalFloatRef yaw) { + if (!this.options.getCameraType().isFirstPerson()) yaw.set(value); + else op.call(player, value); + } + + @WrapOperation( + method = "grabPanoramixScreenshot", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;setYRot(F)V") + ) + private void applyThirdPersonCameraRotation(LocalPlayer player, float value, Operation op, @Share("yaw") LocalFloatRef yaw) { + if (!this.options.getCameraType().isFirstPerson()) + ((CameraAccessor) this.gameRenderer.getMainCamera()).invokeSetRotation(yaw.get(), value); + else op.call(player, value); + } + + @ModifyConstant( + method = "grabPanoramixScreenshot", + constant = @Constant(intValue = 4096) + ) + private int configurablePanoramaSize(int original) { + return SnapperConfig.HOLDER.get().panoramaDimensions().size() * 4; + } + +} diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/PauseScreenMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/PauseScreenMixin.java new file mode 100644 index 0000000..6aabaa8 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/mixin/PauseScreenMixin.java @@ -0,0 +1,44 @@ +package dev.spiritstudios.snapper.mixin; + +import dev.spiritstudios.snapper.Snapper; +import dev.spiritstudios.snapper.SnapperConfig; +import dev.spiritstudios.snapper.gui.screen.ScreenshotListScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.SpriteIconButton; +import net.minecraft.client.gui.screens.PauseScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PauseScreen.class) +public abstract class PauseScreenMixin extends Screen { + protected PauseScreenMixin(Component title) { + super(title); + } + + @Unique + private static final ResourceLocation SNAPPER_BUTTON_ICON = Snapper.id("screenshots/screenshot"); + + @Inject( + method = "init", + at = @At("TAIL") + ) + protected void initWidgets(CallbackInfo ci) { + if (SnapperConfig.HOLDER.get().snapperButton().showInGameMenu()) { + this.addRenderableWidget( + SpriteIconButton.builder( + Component.translatable("button.snapper.screenshots"), + button -> { + Minecraft.getInstance().setScreen(new ScreenshotListScreen(new PauseScreen(true))); + }, + true + ).width(20).sprite(SNAPPER_BUTTON_ICON, 15, 15).build() + ).setPosition(this.width / 2 - 130, height / 4 + 32); + } + } +} diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/ScreenshotRecorderMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/ScreenshotRecorderMixin.java index 109727e..818df58 100644 --- a/src/client/java/dev/spiritstudios/snapper/mixin/ScreenshotRecorderMixin.java +++ b/src/client/java/dev/spiritstudios/snapper/mixin/ScreenshotRecorderMixin.java @@ -2,12 +2,13 @@ import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.mojang.blaze3d.pipeline.RenderTarget; +import com.mojang.blaze3d.platform.NativeImage; import dev.spiritstudios.snapper.Snapper; import dev.spiritstudios.snapper.SnapperConfig; -import net.minecraft.client.gl.Framebuffer; -import net.minecraft.client.texture.NativeImage; -import net.minecraft.client.util.ScreenshotRecorder; -import net.minecraft.text.Text; +import dev.spiritstudios.snapper.util.PlatformHelper; +import net.minecraft.client.Screenshot; +import net.minecraft.network.chat.Component; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -19,7 +20,7 @@ import java.nio.file.Files; import java.util.function.Consumer; -@Mixin(ScreenshotRecorder.class) +@Mixin(Screenshot.class) public abstract class ScreenshotRecorderMixin { /** * @author hama @@ -28,41 +29,41 @@ public abstract class ScreenshotRecorderMixin { @SuppressWarnings("ResultOfMethodCallIgnored") @Inject( method = "method_22691", - at = @At(value = "INVOKE", target = "Lnet/minecraft/client/texture/NativeImage;writeTo(Ljava/io/File;)V") + at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/NativeImage;writeToFile(Ljava/io/File;)V") ) - private static void lookBeforeYouLeap(NativeImage nativeImage, File screenshotFile, Consumer messageReceiver, CallbackInfo ci) throws IOException { + private static void lookBeforeYouLeap(NativeImage nativeImage, File screenshotFile, Consumer messageReceiver, CallbackInfo ci) throws IOException { screenshotFile.getParentFile().mkdirs(); screenshotFile.createNewFile(); } @Inject( method = "method_22691", - at = @At(value = "INVOKE", target = "Lnet/minecraft/text/Text;literal(Ljava/lang/String;)Lnet/minecraft/text/MutableText;", shift = At.Shift.AFTER) + at = @At(value = "INVOKE", target = "Lnet/minecraft/network/chat/Component;literal(Ljava/lang/String;)Lnet/minecraft/network/chat/MutableComponent;", shift = At.Shift.AFTER) ) - private static void saveWrittenFileToClipboard(NativeImage nativeImage, File screenshotFile, Consumer messageReceiver, CallbackInfo ci) { - if (!screenshotFile.getAbsolutePath().contains("/panorama/") && SnapperConfig.INSTANCE.copyTakenScreenshot.get()) { - Snapper.getPlatformHelper().copyScreenshot(screenshotFile.toPath()); + private static void saveWrittenFileToClipboard(NativeImage nativeImage, File screenshotFile, Consumer messageReceiver, CallbackInfo ci) { + if (!screenshotFile.getAbsolutePath().contains("/panorama/") && SnapperConfig.HOLDER.get().copyTakenScreenshot()) { + PlatformHelper.INSTANCE.copyScreenshot(screenshotFile.toPath()); } } /** * @author WorldWidePixel - * @reason Okay, I know this is weird but it's so we can use our own wrapper text for the toast. + * @reason Okay, I know this is weird but it's so we can use our own wrapper text for the push. */ @ModifyArg(method = "method_22691", - at = @At(value = "INVOKE", target = "Lnet/minecraft/text/Text;translatable(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/text/MutableText;", ordinal = 0)) + at = @At(value = "INVOKE", target = "Lnet/minecraft/network/chat/Component;translatable(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/network/chat/MutableComponent;", ordinal = 0)) private static String changeSuccessTranslation(String existing) { return "toast.snapper.screenshot.created.success"; } - @WrapMethod(method = "saveScreenshot(Ljava/io/File;Ljava/lang/String;Lnet/minecraft/client/gl/Framebuffer;ILjava/util/function/Consumer;)V") - private static void getConfiguredGameDirectory(File gameDirectory, String fileName, Framebuffer framebuffer, int downscaleFactor, Consumer messageReceiver, Operation original) { + @WrapMethod(method = "grab(Ljava/io/File;Ljava/lang/String;Lcom/mojang/blaze3d/pipeline/RenderTarget;ILjava/util/function/Consumer;)V") + private static void getConfiguredGameDirectory(File gameDirectory, String fileName, RenderTarget renderTarget, int downscaleFactor, Consumer messageReceiver, Operation original) { original.call( - SnapperConfig.INSTANCE.useCustomScreenshotFolder.get() && Files.exists(SnapperConfig.INSTANCE.customScreenshotFolder.get()) ? - SnapperConfig.INSTANCE.customScreenshotFolder.get().toFile() : + SnapperConfig.HOLDER.get().customScreenshotPath().enabled() && Files.exists(SnapperConfig.HOLDER.get().customScreenshotPath().path()) ? + SnapperConfig.HOLDER.get().customScreenshotPath().path().toFile() : gameDirectory, fileName, - framebuffer, + renderTarget, downscaleFactor, messageReceiver ); diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/TitleScreenMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/TitleScreenMixin.java index a1dbe7a..110c8eb 100644 --- a/src/client/java/dev/spiritstudios/snapper/mixin/TitleScreenMixin.java +++ b/src/client/java/dev/spiritstudios/snapper/mixin/TitleScreenMixin.java @@ -2,12 +2,12 @@ import dev.spiritstudios.snapper.Snapper; import dev.spiritstudios.snapper.SnapperConfig; -import dev.spiritstudios.snapper.gui.screen.ScreenshotScreen; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.screen.TitleScreen; -import net.minecraft.client.gui.widget.TextIconButtonWidget; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; +import dev.spiritstudios.snapper.gui.screen.ScreenshotListScreen; +import net.minecraft.client.gui.components.SpriteIconButton; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; @@ -19,9 +19,9 @@ @Mixin(TitleScreen.class) public abstract class TitleScreenMixin extends Screen { @Unique - private static final Identifier SNAPPER_BUTTON_ICON = Snapper.id("screenshots/screenshot"); + private static final ResourceLocation SNAPPER_BUTTON_ICON = Snapper.id("screenshots/screenshot"); - protected TitleScreenMixin(Text title) { + protected TitleScreenMixin(Component title) { super(title); } @@ -30,18 +30,18 @@ protected TitleScreenMixin(Text title) { at = @At("HEAD") ) protected void init(CallbackInfo ci) { - if (SnapperConfig.INSTANCE.showSnapperTitleScreen.get()) { - Objects.requireNonNull(client); + if (SnapperConfig.HOLDER.get().snapperButton().showOnTitleScreen()) { + Objects.requireNonNull(minecraft); int y = this.height / 4 + 48; int spacingY = 24; - this.addDrawableChild( - TextIconButtonWidget.builder( - Text.translatable("button.snapper.screenshots"), - button -> this.client.setScreen(new ScreenshotScreen((TitleScreen) ((Object) this))), + this.addRenderableWidget( + SpriteIconButton.builder( + Component.translatable("button.snapper.screenshots"), + button -> this.minecraft.setScreen(new ScreenshotListScreen((TitleScreen) ((Object) this))), true - ).width(20).texture(SNAPPER_BUTTON_ICON, 15, 15).build() + ).width(20).sprite(SNAPPER_BUTTON_ICON, 15, 15).build() ).setPosition(this.width / 2 - 124, y + spacingY); } } diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/accessor/EntryListWidgetAccessor.java b/src/client/java/dev/spiritstudios/snapper/mixin/accessor/AbstractSelectionListAccessor.java similarity index 52% rename from src/client/java/dev/spiritstudios/snapper/mixin/accessor/EntryListWidgetAccessor.java rename to src/client/java/dev/spiritstudios/snapper/mixin/accessor/AbstractSelectionListAccessor.java index 4876a00..9359141 100644 --- a/src/client/java/dev/spiritstudios/snapper/mixin/accessor/EntryListWidgetAccessor.java +++ b/src/client/java/dev/spiritstudios/snapper/mixin/accessor/AbstractSelectionListAccessor.java @@ -1,13 +1,13 @@ package dev.spiritstudios.snapper.mixin.accessor; -import net.minecraft.client.gui.widget.EntryListWidget; +import net.minecraft.client.gui.components.AbstractSelectionList; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mutable; import org.spongepowered.asm.mixin.gen.Accessor; -@Mixin(EntryListWidget.class) -public interface EntryListWidgetAccessor { +@Mixin(AbstractSelectionList.class) +public interface AbstractSelectionListAccessor { @Mutable @Accessor - void setItemHeight(int height); + void setDefaultEntryHeight(int height); } diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/accessor/AbstractSliderButtonAccessor.java b/src/client/java/dev/spiritstudios/snapper/mixin/accessor/AbstractSliderButtonAccessor.java new file mode 100644 index 0000000..804f4f5 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/mixin/accessor/AbstractSliderButtonAccessor.java @@ -0,0 +1,14 @@ +package dev.spiritstudios.snapper.mixin.accessor; + +import net.minecraft.client.gui.components.AbstractSliderButton; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(AbstractSliderButton.class) +public interface AbstractSliderButtonAccessor { + @Accessor + boolean getCanChangeValue(); + + @Accessor + void setCanChangeValue(boolean value); +} diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/accessor/CameraAccessor.java b/src/client/java/dev/spiritstudios/snapper/mixin/accessor/CameraAccessor.java index 222e6e3..162dd4c 100644 --- a/src/client/java/dev/spiritstudios/snapper/mixin/accessor/CameraAccessor.java +++ b/src/client/java/dev/spiritstudios/snapper/mixin/accessor/CameraAccessor.java @@ -1,6 +1,6 @@ package dev.spiritstudios.snapper.mixin.accessor; -import net.minecraft.client.render.Camera; +import net.minecraft.client.Camera; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Invoker; diff --git a/src/client/java/dev/spiritstudios/snapper/util/config/DirectoryConfigUtil.java b/src/client/java/dev/spiritstudios/snapper/util/DirectoryConfigUtil.java similarity index 64% rename from src/client/java/dev/spiritstudios/snapper/util/config/DirectoryConfigUtil.java rename to src/client/java/dev/spiritstudios/snapper/util/DirectoryConfigUtil.java index b663628..ecfa035 100644 --- a/src/client/java/dev/spiritstudios/snapper/util/config/DirectoryConfigUtil.java +++ b/src/client/java/dev/spiritstudios/snapper/util/DirectoryConfigUtil.java @@ -1,11 +1,8 @@ -package dev.spiritstudios.snapper.util.config; +package dev.spiritstudios.snapper.util; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; -import dev.spiritstudios.snapper.gui.widget.FolderSelectWidget; -import dev.spiritstudios.specter.api.config.Value; import joptsimple.internal.Strings; -import net.minecraft.client.gui.widget.ClickableWidget; import org.apache.commons.lang3.SystemProperties; import org.lwjgl.util.tinyfd.TinyFileDialogs; @@ -14,7 +11,6 @@ import java.nio.file.Path; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.function.BiFunction; public class DirectoryConfigUtil { public static final Codec PATH_CODEC = Codec.STRING.comapFlatMap( @@ -37,8 +33,13 @@ public class DirectoryConfigUtil { ); public static CompletableFuture> openFolderSelect(String title) { - // replaceAll is to prevent an ACE exploit in TinyFD - return CompletableFuture.supplyAsync(() -> TinyFileDialogs.tinyfd_selectFolderDialog(title.replaceAll("[^a-zA-Z0-9 .,]", ""), SystemProperties.getUserHome())) + // replaceAll is to prevent an ACE exploit in TinyFD + return CompletableFuture.supplyAsync( + () -> TinyFileDialogs.tinyfd_selectFolderDialog( + title.replaceAll("[^a-zA-Z0-9 .,]", ""), + SystemProperties.getUserHome() + ) + ) .thenApply(selectedPath -> { if (Strings.isNullOrEmpty(selectedPath)) { return Optional.empty(); @@ -48,12 +49,6 @@ public static CompletableFuture> openFolderSelect(String title) { }); } - public static final BiFunction, String, ? extends ClickableWidget> PATH_WIDGET_FACTORY = (configValue, id) -> { - @SuppressWarnings("unchecked") Value value = (Value) configValue; - - return new FolderSelectWidget(0, 0, 10, 10, value, "%s.placeholder".formatted(configValue.translationKey(id))); - }; - public static String escapePath(String path) { return path.replace("\\", "\\\\"); } diff --git a/src/client/java/dev/spiritstudios/snapper/util/DynamicCubemapTexture.java b/src/client/java/dev/spiritstudios/snapper/util/DynamicCubemapTexture.java index a10d17a..0020af5 100644 --- a/src/client/java/dev/spiritstudios/snapper/util/DynamicCubemapTexture.java +++ b/src/client/java/dev/spiritstudios/snapper/util/DynamicCubemapTexture.java @@ -1,12 +1,12 @@ package dev.spiritstudios.snapper.util; +import com.mojang.blaze3d.platform.NativeImage; import dev.spiritstudios.snapper.Snapper; -import net.minecraft.client.resource.metadata.TextureResourceMetadata; -import net.minecraft.client.texture.CubemapTexture; -import net.minecraft.client.texture.NativeImage; -import net.minecraft.client.texture.TextureContents; -import net.minecraft.resource.ResourceManager; -import net.minecraft.util.Identifier; +import net.minecraft.client.renderer.texture.CubeMapTexture; +import net.minecraft.client.renderer.texture.TextureContents; +import net.minecraft.client.resources.metadata.texture.TextureMetadataSection; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; import java.io.IOException; import java.io.InputStream; @@ -14,11 +14,11 @@ import java.nio.file.Path; import java.util.Optional; -public class DynamicCubemapTexture extends CubemapTexture { +public class DynamicCubemapTexture extends CubeMapTexture { private static final String[] TEXTURE_SUFFIXES = new String[]{"_1.png", "_3.png", "_5.png", "_4.png", "_0.png", "_2.png"}; private final Path path; - public DynamicCubemapTexture(Identifier id, Path path) { + public DynamicCubemapTexture(ResourceLocation id, Path path) { super(id); this.path = path; } @@ -37,7 +37,7 @@ public TextureContents loadContents(ResourceManager resourceManager) throws IOEx try (InputStream panoramaStream = Files.newInputStream(path.resolve("panorama" + TEXTURE_SUFFIXES[i]))) { NativeImage panoramaImage = NativeImage.read(panoramaStream); if (panoramaImage.getWidth() != width || panoramaImage.getHeight() != height) { - Snapper.LOGGER.error("Image dimensions of panorama '{}' sides do not match: part 0 is {}x{}, but part {} is {}x{}", getId(), width, height, i, panoramaImage.getWidth(), panoramaImage.getHeight()); + Snapper.LOGGER.error("Image dimensions of panorama '{}' sides do not match: part 0 is {}x{}, but part {} is {}x{}", getTexture(), width, height, i, panoramaImage.getWidth(), panoramaImage.getHeight()); baseImage.close(); throw new IOException(); } @@ -47,12 +47,12 @@ public TextureContents loadContents(ResourceManager resourceManager) throws IOEx } baseImage.close(); - contents = new TextureContents(image, new TextureResourceMetadata(true, false)); + contents = new TextureContents(image, new TextureMetadataSection(true, false)); } return contents; } - public static Optional createPanorama(Identifier id, Path path) { + public static Optional createPanorama(ResourceLocation id, Path path) { return Optional.of(new DynamicCubemapTexture( id, path diff --git a/src/client/java/dev/spiritstudios/snapper/util/DynamicTexture.java b/src/client/java/dev/spiritstudios/snapper/util/DynamicTexture.java deleted file mode 100644 index d8783f4..0000000 --- a/src/client/java/dev/spiritstudios/snapper/util/DynamicTexture.java +++ /dev/null @@ -1,95 +0,0 @@ -package dev.spiritstudios.snapper.util; - -import dev.spiritstudios.snapper.Snapper; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.texture.NativeImage; -import net.minecraft.client.texture.NativeImageBackedTexture; -import net.minecraft.client.texture.TextureManager; -import net.minecraft.util.Identifier; -import net.minecraft.util.Util; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; - -public class DynamicTexture implements AutoCloseable { - private static final Identifier UNKNOWN_SERVER = Identifier.ofVanilla("textures/misc/unknown_server.png"); - - private final TextureManager textureManager; - private final Identifier id; - private final Path path; - - private final NativeImage image; - private NativeImageBackedTexture texture; - - private DynamicTexture(TextureManager textureManager, Identifier id, Path path) throws IOException { - this.textureManager = textureManager; - this.id = id; - - this.path = path; - - try (InputStream stream = Files.newInputStream(path)) { - this.image = NativeImage.read(stream); - } - } - - public CompletableFuture load() { - return MinecraftClient.getInstance().submit(() -> { - this.texture = new NativeImageBackedTexture(this.id::toString, this.image); - this.textureManager.registerTexture(this.id, this.texture); - }); - } - - public static Optional createScreenshot(TextureManager textureManager, Path path) { - try { - return Optional.of(new DynamicTexture( - textureManager, - Snapper.id( - "screenshots/" + Util.replaceInvalidChars(path.getFileName().toString(), Identifier::isPathCharacterValid) + "/icon" - ), - path - )); - } catch (IOException e) { - return Optional.empty(); - } - } - - /* - * Must be called on render thread - */ - public void enableFiltering() { - this.texture.setFilter(true, true); - } - - public void destroy() { - this.textureManager.destroyTexture(this.id); - this.texture.close(); - } - - public int getWidth() { - return this.texture != null && this.texture.getImage() != null ? this.texture.getImage().getWidth() : 64; - } - - public int getHeight() { - return this.texture != null && this.texture.getImage() != null ? this.texture.getImage().getHeight() : 64; - } - - public Identifier getTextureId() { - return this.texture != null ? this.id : UNKNOWN_SERVER; - } - - public boolean loaded() { - return texture != null; - } - - public Path getPath() { - return path; - } - - public void close() { - this.destroy(); - } -} diff --git a/src/client/java/dev/spiritstudios/snapper/util/PlatformHelper.java b/src/client/java/dev/spiritstudios/snapper/util/PlatformHelper.java index 876d0ba..0597a91 100644 --- a/src/client/java/dev/spiritstudios/snapper/util/PlatformHelper.java +++ b/src/client/java/dev/spiritstudios/snapper/util/PlatformHelper.java @@ -1,7 +1,15 @@ package dev.spiritstudios.snapper.util; +import dev.spiritstudios.snapper.util.actions.GeneralPlatformActions; +import dev.spiritstudios.snapper.util.actions.MacPlatformActions; +import net.minecraft.Util; + import java.nio.file.Path; public interface PlatformHelper { + PlatformHelper INSTANCE = Util.getPlatform() == Util.OS.OSX ? + new MacPlatformActions() : + new GeneralPlatformActions(); + void copyScreenshot(Path screenshot); } diff --git a/src/client/java/dev/spiritstudios/snapper/util/Screenshooter.java b/src/client/java/dev/spiritstudios/snapper/util/Screenshooter.java new file mode 100644 index 0000000..6418fdc --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/util/Screenshooter.java @@ -0,0 +1,6 @@ +package dev.spiritstudios.snapper.util; + +public class Screenshooter { + public void youJustWonTheGame() {} + // <3 lynn +} diff --git a/src/client/java/dev/spiritstudios/snapper/util/ScreenshotActions.java b/src/client/java/dev/spiritstudios/snapper/util/ScreenshotActions.java index ac78a35..f1712bc 100644 --- a/src/client/java/dev/spiritstudios/snapper/util/ScreenshotActions.java +++ b/src/client/java/dev/spiritstudios/snapper/util/ScreenshotActions.java @@ -1,12 +1,12 @@ package dev.spiritstudios.snapper.util; import dev.spiritstudios.snapper.Snapper; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.ConfirmScreen; -import net.minecraft.client.gui.screen.ProgressScreen; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.screen.ScreenTexts; -import net.minecraft.text.Text; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.ProgressScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; import java.io.IOException; import java.nio.file.Files; @@ -20,7 +20,7 @@ public class ScreenshotActions { public static void deleteScreenshot(Path path, Screen screen) { if (!Files.exists(path)) return; - MinecraftClient client = MinecraftClient.getInstance(); + Minecraft client = Minecraft.getInstance(); client.setScreen( new ConfirmScreen( confirmed -> { @@ -34,10 +34,10 @@ public static void deleteScreenshot(Path path, Screen screen) { } client.setScreen(screen); }, - Text.translatable("text.snapper.delete_question"), - Text.translatable("text.snapper.delete_warning", path.getFileName()), - Text.translatable("button.snapper.delete"), - ScreenTexts.CANCEL + Component.translatable("text.snapper.delete_question"), + Component.translatable("text.snapper.delete_warning", path.getFileName()), + Component.translatable("button.snapper.delete"), + CommonComponents.GUI_CANCEL ) ); } diff --git a/src/client/java/dev/spiritstudios/snapper/util/ScreenshotTexture.java b/src/client/java/dev/spiritstudios/snapper/util/ScreenshotTexture.java new file mode 100644 index 0000000..3070105 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/util/ScreenshotTexture.java @@ -0,0 +1,95 @@ +package dev.spiritstudios.snapper.util; + +import com.mojang.blaze3d.platform.NativeImage; +import dev.spiritstudios.snapper.Snapper; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.resources.ResourceLocation; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public class ScreenshotTexture implements AutoCloseable { + private static final ResourceLocation UNKNOWN_SERVER = ResourceLocation.withDefaultNamespace("textures/misc/unknown_server.png"); + + private final TextureManager textureManager; + private final ResourceLocation id; + private final Path path; + + private final NativeImage image; + private DynamicTexture texture; + + private ScreenshotTexture(TextureManager textureManager, ResourceLocation id, Path path) throws IOException { + this.textureManager = textureManager; + this.id = id; + + this.path = path; + + try (InputStream stream = Files.newInputStream(path)) { + this.image = NativeImage.read(stream); + } + } + + public CompletableFuture load() { + return Minecraft.getInstance().submit(() -> { + this.texture = new DynamicTexture(this.id::toString, this.image); + this.textureManager.register(this.id, this.texture); + }); + } + + public static Optional createScreenshot(TextureManager textureManager, Path path) { + try { + return Optional.of(new ScreenshotTexture( + textureManager, + Snapper.id( + "screenshots/" + Util.sanitizeName(path.getFileName().toString(), ResourceLocation::validPathChar) + "/icon" + ), + path + )); + } catch (IOException e) { + return Optional.empty(); + } + } + + /* + * Must be called on render thread + */ + public void enableFiltering() { + this.texture.setFilter(true, true); + } + + public void destroy() { + this.textureManager.release(this.id); + this.texture.close(); + } + + public int getWidth() { + return this.texture != null ? this.texture.getTexture().getWidth(0) : 64; + } + + public int getHeight() { + return this.texture != null ? this.texture.getTexture().getHeight(0) : 64; + } + + public ResourceLocation getTextureId() { + return this.texture != null ? this.id : UNKNOWN_SERVER; + } + + public boolean loaded() { + return texture != null; + } + + public Path getPath() { + return path; + } + + public void close() { + this.destroy(); + } +} diff --git a/src/client/java/dev/spiritstudios/snapper/util/SnapperUtil.java b/src/client/java/dev/spiritstudios/snapper/util/SnapperUtil.java index 48042f8..7e392f7 100644 --- a/src/client/java/dev/spiritstudios/snapper/util/SnapperUtil.java +++ b/src/client/java/dev/spiritstudios/snapper/util/SnapperUtil.java @@ -1,22 +1,30 @@ package dev.spiritstudios.snapper.util; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; import dev.spiritstudios.snapper.Snapper; import dev.spiritstudios.snapper.SnapperConfig; -import dev.spiritstudios.snapper.gui.toast.SnapperToast; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Text; -import net.minecraft.util.Util; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.util.FormattedCharSequence; import org.apache.commons.lang3.SystemProperties; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; +import java.util.stream.Collectors; public final class SnapperUtil { // Helper things. Please order alphabetically. <3 Lynn public static Path getConfiguredScreenshotDirectory() { - if (SnapperConfig.INSTANCE.useCustomScreenshotFolder.get()) { - Path customPath = SnapperConfig.INSTANCE.customScreenshotFolder.get().resolve("screenshots"); + if (SnapperConfig.HOLDER.get().customScreenshotPath().enabled()) { + Path customPath = SnapperConfig.HOLDER.get().customScreenshotPath().path().resolve("screenshots"); if (!SafeFiles.createDirectories(customPath)) { Snapper.LOGGER.error("Failed to create directories of configured custom screenshot folder"); @@ -25,15 +33,22 @@ public static Path getConfiguredScreenshotDirectory() { return customPath; } - return MinecraftClient.getInstance().runDirectory.toPath().resolve("screenshots"); + return Minecraft.getInstance().gameDirectory.toPath().resolve("screenshots"); } public static boolean inBoundingBox(int x, int y, int w, int h, double mouseX, double mouseY) { return mouseX > x && mouseX < x + w && mouseY > y && mouseY < y + h; } + public static FormattedCharSequence clipText(Font font, Component message, int width) { + if (font.width(message) < width) return message.getVisualOrderText(); + + FormattedText formattedText = font.substrByWidth(message, width - font.width(CommonComponents.ELLIPSIS)); + return Language.getInstance().getVisualOrder(FormattedText.composite(formattedText, CommonComponents.ELLIPSIS)); + } + public static boolean isOfflineAccount() { - return MinecraftClient.getInstance().getSession().getAccessToken().length() < 400; + return Minecraft.getInstance().getUser().getAccessToken().length() < 400; } public static boolean panoramaPresent(Path path) { @@ -49,6 +64,20 @@ public static boolean panoramaPresent(Path path) { public enum PanoramaSize { ONE_THOUSAND_TWENTY_FOUR(1024), TWO_THOUSAND_FORTY_EIGHT(2048), FOUR_THOUSAND_NINETY_SIX(4096); + public static final Codec CODEC = Codec.INT.comapFlatMap( + i -> { + for (PanoramaSize size : PanoramaSize.values()) { + if (i == size.size) { + return DataResult.success(size); + } + } + return DataResult.error(() -> "Invalid panorama size, must be one of " + Arrays.stream(PanoramaSize.values()) + .map(panoramaSize -> Integer.toString(panoramaSize.size)) + .collect(Collectors.joining(",")) + ); + }, + PanoramaSize::size + ); private final int size; PanoramaSize(int size) { @@ -60,17 +89,7 @@ public int size() { } } - public static void toast(SnapperToast.Type type, Text title, Text description) { - MinecraftClient.getInstance().getToastManager().add( - new SnapperToast( - type, - title, - description - ) - ); - } - - public static final Path UNIFIED_FOLDER = switch (Util.getOperatingSystem()) { + public static final Path UNIFIED_FOLDER = switch (Util.getPlatform()) { case WINDOWS -> Path.of(System.getenv("APPDATA"), ".snapper"); case OSX -> Path.of(SystemProperties.getUserHome(), "Library", "Application Support", "snapper"); default -> Path.of(SystemProperties.getUserHome(), ".snapper"); diff --git a/src/client/java/dev/spiritstudios/snapper/util/uploading/AxolotlAuthentication.java b/src/client/java/dev/spiritstudios/snapper/util/uploading/AxolotlAuthentication.java index 3f17cff..0ff362f 100644 --- a/src/client/java/dev/spiritstudios/snapper/util/uploading/AxolotlAuthentication.java +++ b/src/client/java/dev/spiritstudios/snapper/util/uploading/AxolotlAuthentication.java @@ -2,14 +2,14 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; -import net.minecraft.util.Uuids; +import net.minecraft.core.UUIDUtil; import java.util.UUID; public record AxolotlAuthentication(String username, UUID uuid, String accessToken) { public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( Codec.STRING.fieldOf("username").forGetter(AxolotlAuthentication::username), - Uuids.CODEC.fieldOf("uuid").forGetter(AxolotlAuthentication::uuid), + UUIDUtil.LENIENT_CODEC.fieldOf("uuid").forGetter(AxolotlAuthentication::uuid), Codec.STRING.fieldOf("access_token").forGetter(AxolotlAuthentication::accessToken) ).apply(instance, AxolotlAuthentication::new)); } diff --git a/src/client/java/dev/spiritstudios/snapper/util/uploading/AxolotlClientApi.java b/src/client/java/dev/spiritstudios/snapper/util/uploading/AxolotlClientApi.java index c8bbefb..56307ca 100644 --- a/src/client/java/dev/spiritstudios/snapper/util/uploading/AxolotlClientApi.java +++ b/src/client/java/dev/spiritstudios/snapper/util/uploading/AxolotlClientApi.java @@ -3,18 +3,20 @@ import com.google.gson.JsonParser; import com.mojang.authlib.exceptions.AuthenticationException; import com.mojang.authlib.minecraft.MinecraftSessionService; +import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; import dev.spiritstudios.snapper.Snapper; import dev.spiritstudios.snapper.SnapperConfig; import dev.spiritstudios.snapper.gui.toast.SnapperToast; -import dev.spiritstudios.snapper.util.SnapperUtil; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.session.Session; -import net.minecraft.text.Text; -import net.minecraft.util.Util; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.User; +import net.minecraft.network.chat.Component; +import net.minecraft.util.StringRepresentable; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.RandomStringUtils; import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; import java.io.Closeable; import java.io.IOException; @@ -32,17 +34,30 @@ import java.util.concurrent.CompletableFuture; public class AxolotlClientApi implements Closeable { - public enum TermsAcceptance { - ACCEPTED, - DENIED, - UNSET + public enum TermsAcceptance implements StringRepresentable { + ACCEPTED("accepted"), + DENIED("denied"), + UNSET("unset"); + + public static final Codec CODEC = StringRepresentable.fromEnum(TermsAcceptance::values); + + private final String name; + + TermsAcceptance(String name) { + this.name = name; + } + + @Override + public @NonNull String getSerializedName() { + return name; + } } private static final String BASE_URL = "https://api.axolotlclient.com/v1/"; private final HttpClient client = HttpClient.newBuilder() .followRedirects(HttpClient.Redirect.NORMAL) - .executor(Util.getIoWorkerExecutor()) + .executor(Util.ioPool()) .build(); private Instant authTime = Instant.EPOCH; @@ -56,9 +71,9 @@ public CompletableFuture uploadImage(Path image) { return CompletableFuture.failedFuture(e); } - SnapperUtil.toast(SnapperToast.Type.UPLOAD, - Text.translatable("toast.snapper.upload.in_progress"), - Text.translatable("toast.snapper.upload.in_progress.description")); + SnapperToast.push(SnapperToast.Type.UPLOAD, + Component.translatable("toast.snapper.upload.in_progress"), + Component.translatable("toast.snapper.upload.in_progress.description")); return authenticate() .thenCompose(ignored -> post("image/" + image.getFileName().toString(), bytes)) @@ -76,17 +91,17 @@ private CompletableFuture authenticate() { if (authTime.plus(24, ChronoUnit.HOURS).isAfter(Instant.now())) return CompletableFuture.completedFuture(null); - Session session = MinecraftClient.getInstance().getSession(); - MinecraftSessionService sessionService = MinecraftClient.getInstance().getSessionService(); + User session = Minecraft.getInstance().getUser(); + MinecraftSessionService sessionService = Minecraft.getInstance().services().sessionService(); String serverId = new BigInteger(DigestUtils.sha1(RandomStringUtils.insecure().next(40).getBytes(StandardCharsets.UTF_8))).toString(16); try { - sessionService.joinServer(session.getUuidOrNull(), session.getAccessToken(), serverId); + sessionService.joinServer(session.getProfileId(), session.getAccessToken(), serverId); } catch (AuthenticationException e) { return CompletableFuture.failedFuture(e); } - return this.get("authenticate", Map.of("username", session.getUsername(), "server_id", serverId)) + return this.get("authenticate", Map.of("username", session.getName(), "server_id", serverId)) .thenApply(response -> AxolotlAuthentication.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(response.body())) .getOrThrow()) @@ -105,7 +120,7 @@ public CompletableFuture> post(String route, byte[] rawBody } private CompletableFuture> request(String route, Map query, byte[] rawBody, String method) { - if (SnapperConfig.INSTANCE.termsAccepted.get() != TermsAcceptance.ACCEPTED) + if (SnapperConfig.HOLDER.get().axolotlClient().termsStatus() != TermsAcceptance.ACCEPTED) return CompletableFuture.failedFuture(new IllegalStateException("Terms not accepted")); StringBuilder url = new StringBuilder(BASE_URL); diff --git a/src/client/java/dev/spiritstudios/snapper/util/uploading/ScreenshotUploading.java b/src/client/java/dev/spiritstudios/snapper/util/uploading/ScreenshotUploading.java index 0fc30b0..5592825 100644 --- a/src/client/java/dev/spiritstudios/snapper/util/uploading/ScreenshotUploading.java +++ b/src/client/java/dev/spiritstudios/snapper/util/uploading/ScreenshotUploading.java @@ -6,15 +6,15 @@ import dev.spiritstudios.snapper.gui.toast.SnapperToast; import dev.spiritstudios.snapper.util.SnapperUtil; import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Text; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; import java.nio.file.Path; import java.util.concurrent.CompletableFuture; public class ScreenshotUploading { public static final String SNAPPER_WEB_URL = "https://snapper.spiritstudios.dev/img/%s"; - public static final String SNAPPER_VERSION = FabricLoader.getInstance().getModContainer("snapper") + public static final String SNAPPER_VERSION = FabricLoader.getInstance().getModContainer(Snapper.MOD_ID) .orElseThrow() .getMetadata().getVersion().getFriendlyString(); @@ -22,30 +22,30 @@ public class ScreenshotUploading { public static CompletableFuture upload(Path image) { if (SnapperUtil.isOfflineAccount()) { - SnapperUtil.toast( + SnapperToast.push( SnapperToast.Type.DENY, - Text.translatable("toast.snapper.upload.axolotlclient.api_disabled"), - Text.translatable("toast.snapper.upload.offline") + Component.translatable("toast.snapper.upload.axolotlclient.api_disabled"), + Component.translatable("toast.snapper.upload.offline") ); return CompletableFuture.failedFuture(new IllegalStateException("Minecraft is currently running in offline mode.")); } - if (SnapperConfig.INSTANCE.termsAccepted.get() == AxolotlClientApi.TermsAcceptance.UNSET) { - MinecraftClient client = MinecraftClient.getInstance(); + if (SnapperConfig.HOLDER.get().axolotlClient().termsStatus() == AxolotlClientApi.TermsAcceptance.UNSET) { + Minecraft client = Minecraft.getInstance(); CompletableFuture success = new CompletableFuture<>(); - client.setScreen(new PrivacyNoticeScreen(client.currentScreen, v -> { + client.setScreen(new PrivacyNoticeScreen(client.screen, v -> { if (v) upload(image).thenAccept(success::complete); })); return success; } - if (SnapperConfig.INSTANCE.termsAccepted.get() != AxolotlClientApi.TermsAcceptance.ACCEPTED) { - SnapperUtil.toast( + if (SnapperConfig.HOLDER.get().axolotlClient().termsStatus() != AxolotlClientApi.TermsAcceptance.ACCEPTED) { + SnapperToast.push( SnapperToast.Type.UPLOAD, - Text.translatable("toast.snapper.upload.failure"), - Text.translatable("toast.snapper.upload.axolotlclient.api_disabled") + Component.translatable("toast.snapper.upload.failure"), + Component.translatable("toast.snapper.upload.axolotlclient.api_disabled") ); return CompletableFuture.failedFuture(new IllegalStateException("AxolotlClient API is disabled.")); } @@ -57,24 +57,24 @@ public static CompletableFuture upload(Path image) { private static void imageUploaded(String imageId) { if (imageId == null) { - SnapperUtil.toast( + SnapperToast.push( SnapperToast.Type.DENY, - Text.translatable("toast.snapper.upload.failure"), - Text.translatable("toast.snapper.upload.failure.generic") + Component.translatable("toast.snapper.upload.failure"), + Component.translatable("toast.snapper.upload.failure.generic") ); return; } - MinecraftClient client = MinecraftClient.getInstance(); + Minecraft client = Minecraft.getInstance(); String snapperUrl = SNAPPER_WEB_URL.formatted(imageId); Snapper.LOGGER.info("Uploaded screenshot to: {}", snapperUrl); - client.keyboard.setClipboard(snapperUrl); - SnapperUtil.toast( + client.keyboardHandler.setClipboard(snapperUrl); + SnapperToast.push( SnapperToast.Type.UPLOAD, - Text.translatable("toast.snapper.upload.success"), - Text.translatable("toast.snapper.upload.success.description", snapperUrl) + Component.translatable("toast.snapper.upload.success"), + Component.translatable("toast.snapper.upload.success.description", snapperUrl) ); } diff --git a/src/client/resources/assets/snapper/lang/de_de.json b/src/client/resources/assets/snapper/lang/de_de.json new file mode 100644 index 0000000..9e6216b --- /dev/null +++ b/src/client/resources/assets/snapper/lang/de_de.json @@ -0,0 +1,96 @@ +{ + "key.category.snapper.snapper": "Snapper", + "key.snapper.panorama": "Panorama-Screenshots aufnehmen", + "key.snapper.huge": "Riesigen Screenshot aufnehmen", + "key.snapper.screenshot_menu": "Screenshots ansehen", + "key.snapper.recent": "Öffne letzten Screenshot", + "menu.snapper.screenshot_menu": "Screenshots", + "menu.snapper.viewer_menu": "Screenshot Browser", + "menu.snapper.panorama": "Panorama ansehen", + "button.snapper.folder": "Ordner öffnen", + "button.snapper.panorama.tooltip": "Das aktuelle Panorama ansehen.", + "button.snapper.screenshots": "Screenshots ansehen", + "button.snapper.delete": "Löschen", + "button.snapper.open": "Extern öffnen", + "button.snapper.view": "Anschauen", + "button.snapper.copy": "Kopieren", + "button.snapper.rename": "Umbenennen", + "button.snapper.upload": "Hochladen", + "button.snapper.upload.tooltip": "Kann offline keine Screenshots hochladen", + "text.snapper.created": "Erstellt am", + "text.snapper.generic": "Screenshot %d", + "text.snapper.loading": "Screenshots werden geladen", + "text.snapper.empty": "Nimm im Spiel einen Screenshot auf, um ihn hier zu sehen!", + "text.snapper.empty.custom": "Du kannst deine Screenshots nicht finden? Versuch, den benutzerdefinierten Screenshot-Ordner zurückzusetzen.", + "text.snapper.panorama_encourage": "Nimm im Spiel einen Panorama-Screenshot auf, um ihn hier zu sehen!", + "text.snapper.delete_question": "Bist du sicher, dass du diesen Screenshot löschen möchtest?", + "text.snapper.delete_warning": "‚%s‘ wird für immer verloren sein! (Eine lange Zeit!)", + "text.snapper.rename": "Screenshot umbenennen", + "text.snapper.rename_input": "Neuen Namen hier eingeben", + "text.snapper.rename_invalid": "Neuer Name des Screenshots ungültig", + "text.snapper.rename_invalid_png": "Name muss mit ‚.png‘ enden", + "config.snapper.title": "Snapper-Einstellungen", + "config.snapper.copyTakenScreenshot": "Auto-Zwischenablage", + "config.snapper.copyTakenScreenshot.tooltip": "Ob Screenshots nach dem Aufnehmen in die Zwischenablage kopiert werden sollen", + "config.snapper.showOnTitleScreen": "Auf dem Titelmenü anzeigen", + "config.snapper.showOnTitleScreen.tooltip": "Ob der Snapper-Knopf auf dem Titelmenü angezeigt werden soll", + "config.snapper.showInGameMenu": "Spielmenü Knopf", + "config.snapper.showInGameMenu.tooltip": "Ob der Snapper-Knopf im Spielmenü angezeigt werden soll", + "config.snapper.viewMode": "Anzeige-Modus", + "config.snapper.viewMode.tooltip": "Ob das Screenshot-Menü als Raster oder Liste angezeigt werden soll", + "config.snapper.viewMode.grid": "Raster", + "config.snapper.viewMode.list": "Liste", + "config.snapper.termsAccepted": "Datenschutzrichtlinien", + "config.snapper.termsAccepted.unset": "Unentschieden", + "config.snapper.termsAccepted.accepted": "Akzeptiert", + "config.snapper.termsAccepted.denied": "Abgelehnt", + "config.snapper.termsAccepted.tooltip": "Ob die Bedingungen des AxolotlClients akzeptiert werden. Dies wirkt sich nur auf die Fähigkeit aus, Screenshots hochzuladen.", + "config.snapper.panoramaDimensions": "Panorama-Skalierung", + "config.snapper.panoramaDimensions.tooltip": "Dimensionen der individuellen Panoramabilder beim Speichern.", + "config.snapper.panoramaDimensions.one_thousand_twenty_four": "Klein (1024x)", + "config.snapper.panoramaDimensions.two_thousand_forty_eight": "Medium (2048x)", + "config.snapper.panoramaDimensions.four_thousand_ninety_six": "Groß (4096x)", + "config.snapper.useCustomScreenshotFolder": "Benutzerdefinierter Ordner", + "config.snapper.useCustomScreenshotFolder.tooltip": "Ob ein benutzerdefinierter oder der Standard Screenshot-Ordner genutzt werden soll", + "config.snapper.customScreenshotFolder": "Screenshot-Ordner", + "config.snapper.customScreenshotFolder.placeholder": "Klicken, um den Screenshot-Ordner anzuzeigen", + "config.snapper.customScreenshotFolder.input": "Ordner, in dem alle Screenshots gespeichert und aus dem alle Screenshots geladen werden", + "config.snapper.customScreenshotFolder.select": "Wähle Ordner", + "config.snapper.customScreenshotFolder.reset": "Auf den Standard zurücksetzen", + "toast.snapper.screenshot.created": "Screenshot aufgenommen", + "toast.snapper.screenshot.created.description": "Als %s gespeichert. Drücke %s um den Screenshot anzusehen", + "toast.snapper.screenshot.created.description_copy": "Als %s gespeichert. Automagisch in die Zwischenablage kopiert", + "toast.snapper.screenshot.created.description_in_menu": "Als %s gespeichert. Sichtbar im Screenshot-Menü", + "toast.snapper.screenshot.created.success": "%s", + "toast.snapper.screenshot.recent.failure": "Konnte letzten Screenshot nicht öffnen", + "toast.snapper.screenshot.recent.failure.not_exist": "Nimm einen Screenshot im Spiel auf, um ihn dir anzusehen!", + "toast.snapper.screenshot.recent.failure.generic": "Siehe Logs für mehr Informationen", + "toast.snapper.screenshot.copy": "Screenshot in die Zwischenablage kopiert", + "toast.snapper.screenshot.copy.description": "", + "toast.snapper.panorama.created": "Panorama aufgenommen", + "toast.snapper.panorama.created.description": "Panorama gespeichert. Drücke %s, um es anzusehen", + "toast.snapper.panorama.failed": "Fehler bei Aufnahme des Panoramas", + "toast.snapper.upload.in_progress": "Bild lädt hoch...", + "toast.snapper.upload.in_progress.description": "Das könnte einen Moment dauern", + "toast.snapper.upload.success": "Bild hochgeladen", + "toast.snapper.upload.success.description": "Ein Link zum hochgeladenen Bild wurde in die Zwischenablage kopiert.", + "toast.snapper.upload.failure": "Fehler beim Hochladen des Bildes", + "toast.snapper.upload.axolotlclient.api_disabled": "Du hast die Bedingungen des AxolotlClients nicht akzeptiert, deshalb ist das Hochladen des Bildes nicht möglich.", + "toast.snapper.upload.offline": "Du bist gerade offline.", + "toast.snapper.upload.failure.generic": "Mehr Informationen können in den Logs gefunden werden.", + "prompt.snapper.folder_select": "Wähle einen benutzerdefinierten Screenshot-Ordner für Snapper aus.", + "overlay.snapper.external_dialog.folder": "Ordner-Auswahl Dialog wurde geöffnet. Durch das Klicken von \"Abbrechen\" im Dialog schließen.", + "snapper.privacy_notice.description": "In Snapper erfolgt das Hochladen von Bildern über AxolotlClient. Das heißt, diese Fähigkeit unterliegt auch den Bedingungen des AxolotlClients, welche du für die Nutzung dieser Funktion akzeptieren musst. Diese Entscheidung kann jederzeit geändert werden.", + "snapper.privacy_notice": "Datenschutzhinweis", + "snapper.privacy_notice.accept": "Akzeptieren", + "snapper.privacy_notice.deny": "Ablehnen", + "snapper.privacy_notice.view_terms": "Bedingungen anschauen", + "config.snapper.snapperButton": "Snapper Knopf", + "config.snapper.general": "General", + "config.snapper.uploading": "Uploading", + "config.snapper.customScreenshotFolderEnabled": "Custom Screenshot Folder", + "text.snapper.image_size": "Bildgröße: %dx%d", + "text.snapper.screen_size": "Bildschirmgröße: %dx%d", + "text.snapper.scale_factor": "Skalierungsfaktor: %s", + "text.snapper.scale_size": "Skalierte Größe: %dx%d" +} diff --git a/src/client/resources/assets/snapper/lang/en_us.json b/src/client/resources/assets/snapper/lang/en_us.json index 5004a97..9ad2729 100644 --- a/src/client/resources/assets/snapper/lang/en_us.json +++ b/src/client/resources/assets/snapper/lang/en_us.json @@ -1,5 +1,5 @@ { - "key.categories.snapper": "Snapper", + "key.category.snapper.snapper": "Snapper", "key.snapper.panorama": "Take Panoramic Screenshots", "key.snapper.huge": "Take Huge Screenshot", "key.snapper.screenshot_menu": "View Screenshots", @@ -18,7 +18,7 @@ "button.snapper.upload": "Upload", "button.snapper.upload.tooltip": "Unable to upload screenshots while offline", "text.snapper.created": "Created", - "text.snapper.generic": "Screenshot", + "text.snapper.generic": "Screenshot %d", "text.snapper.loading": "Loading Screenshots", "text.snapper.empty": "Take a screenshot in-game to see it here!", "text.snapper.empty.custom": "Can't find your screenshots? Try disabling custom screenshot folders.", @@ -29,34 +29,37 @@ "text.snapper.rename_input": "Enter new name here", "text.snapper.rename_invalid": "New name for screenshot invalid", "text.snapper.rename_invalid_png": "Name must end with '.png'", - "config.snapper.snapper.title": "Snapper Settings", - "config.snapper.snapper.copyTakenScreenshot": "Auto-Clipboard", - "config.snapper.snapper.copyTakenScreenshot.tooltip": "Copy screenshot to clipboard when taken", - "config.snapper.snapper.showSnapperTitleScreen": "Title Screen Button", - "config.snapper.snapper.showSnapperTitleScreen.tooltip": "Show Snapper button on title screen", - "config.snapper.snapper.showSnapperGameMenu": "Game Menu Button", - "config.snapper.snapper.showSnapperGameMenu.tooltip": "Show Snapper button in game menu", - "config.snapper.snapper.viewMode": "View Mode", - "config.snapper.snapper.viewMode.tooltip": "Show screenshot menu with grid or list", - "config.snapper.snapper.viewMode.grid": "Grid", - "config.snapper.snapper.viewMode.list": "List", - "config.snapper.snapper.termsAccepted": "Privacy Policy", - "config.snapper.snapper.termsAccepted.unset": "Unset", - "config.snapper.snapper.termsAccepted.accepted": "Accepted", - "config.snapper.snapper.termsAccepted.denied": "Denied", - "config.snapper.snapper.termsAccepted.tooltip": "Acceptance of AxolotlClient's terms. This only affects the ability to upload screenshots.", - "config.snapper.snapper.panoramaDimensions": "Panorama Scale", - "config.snapper.snapper.panoramaDimensions.tooltip": "Dimensions of individual panorama images when saved.", - "config.snapper.snapper.panoramaDimensions.one_thousand_twenty_four": "Small (1024x)", - "config.snapper.snapper.panoramaDimensions.two_thousand_forty_eight": "Medium (2048x)", - "config.snapper.snapper.panoramaDimensions.four_thousand_ninety_six": "Large (4096x)", - "config.snapper.snapper.useCustomScreenshotFolder": "Custom Folder", - "config.snapper.snapper.useCustomScreenshotFolder.tooltip": "Whether to use a custom screenshot folder or the default", - "config.snapper.snapper.customScreenshotFolder": "Screenshot Folder", - "config.snapper.snapper.customScreenshotFolder.placeholder": "Click to reveal", - "config.snapper.snapper.customScreenshotFolder.input": "Folder for all screenshots to be saved and viewed from", - "config.snapper.snapper.customScreenshotFolder.select": "Select folder", - "config.snapper.snapper.customScreenshotFolder.reset": "Reset to default", + "config.snapper.title": "Snapper Settings", + "config.snapper.general": "General", + "config.snapper.uploading": "Uploading", + "config.snapper.copyTakenScreenshot": "Auto-Clipboard", + "config.snapper.copyTakenScreenshot.tooltip": "Copy screenshot to clipboard when taken", + "config.snapper.showOnTitleScreen": "Title Screen Button", + "config.snapper.showOnTitleScreen.tooltip": "Show Snapper button on title screen", + "config.snapper.showInGameMenu": "Game Menu Button", + "config.snapper.showInGameMenu.tooltip": "Show Snapper button in game menu", + "config.snapper.viewMode": "View Mode", + "config.snapper.viewMode.tooltip": "Show screenshot menu with grid or list", + "config.snapper.viewMode.grid": "Grid", + "config.snapper.viewMode.list": "List", + "config.snapper.termsAccepted": "Privacy Policy", + "config.snapper.termsAccepted.unset": "Unset", + "config.snapper.termsAccepted.accepted": "Accepted", + "config.snapper.termsAccepted.denied": "Denied", + "config.snapper.termsAccepted.tooltip": "Acceptance of AxolotlClient's terms. This only affects the ability to upload screenshots.", + "config.snapper.panoramaDimensions": "Panorama Scale", + "config.snapper.panoramaDimensions.tooltip": "Dimensions of individual panorama images when saved.", + "config.snapper.panoramaDimensions.one_thousand_twenty_four": "Small (1024x)", + "config.snapper.panoramaDimensions.two_thousand_forty_eight": "Medium (2048x)", + "config.snapper.panoramaDimensions.four_thousand_ninety_six": "Large (4096x)", + "config.snapper.useCustomScreenshotFolder": "Custom Folder", + "config.snapper.useCustomScreenshotFolder.tooltip": "Whether to use a custom screenshot folder or the default", + "config.snapper.customScreenshotFolder": "Screenshot Folder", + "config.snapper.customScreenshotFolderEnabled": "Custom Screenshot Folder", + "config.snapper.customScreenshotFolder.placeholder": "Click to reveal", + "config.snapper.customScreenshotFolder.input": "Folder for all screenshots to be saved and viewed from", + "config.snapper.customScreenshotFolder.select": "Select folder", + "config.snapper.customScreenshotFolder.reset": "Reset to default", "toast.snapper.screenshot.created": "Screenshot captured", "toast.snapper.screenshot.created.description": "Saved as %s. View by pressing %s", "toast.snapper.screenshot.created.description_copy": "Saved as %s. Automagically copied to clipboard", @@ -65,6 +68,8 @@ "toast.snapper.screenshot.recent.failure": "Could not open latest screenshot", "toast.snapper.screenshot.recent.failure.not_exist": "Take a screenshot in order to view it!", "toast.snapper.screenshot.recent.failure.generic": "Check logs for more information", + "toast.snapper.screenshot.copy": "Screenshot copied to clipboard", + "toast.snapper.screenshot.copy.description": "", "toast.snapper.panorama.created": "Panorama captured", "toast.snapper.panorama.created.description": "Panorama saved. View by pressing %s", "toast.snapper.panorama.failed": "Failed to capture panorama", @@ -82,5 +87,11 @@ "snapper.privacy_notice": "Privacy Notice", "snapper.privacy_notice.accept": "Accept", "snapper.privacy_notice.deny": "Deny/Decide Later", - "snapper.privacy_notice.view_terms": "View Terms" + "snapper.privacy_notice.view_terms": "View Terms", + "text.snapper.unknown": "Unknown", + "config.snapper.snapperButton": "Snapper Button", + "text.snapper.image_size": "Image Size: %dx%d", + "text.snapper.screen_size": "Screen Size: %dx%d", + "text.snapper.scale_factor": "Scale Factor: %s", + "text.snapper.scale_size": "Scaled Size: %dx%d" } diff --git a/src/client/resources/snapper.mixins.json b/src/client/resources/snapper.mixins.json index 4330c5b..ccddfaf 100644 --- a/src/client/resources/snapper.mixins.json +++ b/src/client/resources/snapper.mixins.json @@ -4,18 +4,22 @@ "compatibilityLevel": "JAVA_21", "client": [ "CameraMixin", - "GameMenuMixin", - "KeyboardMixin", "InGameHudMixin", - "MinecraftClientMixin", + "KeyboardHandlerMixin", + "MinecraftMixin", + "PauseScreenMixin", "ScreenshotRecorderMixin", "TitleScreenMixin", - "accessor.CameraAccessor", - "accessor.EntryListWidgetAccessor" + "accessor.AbstractSelectionListAccessor", + "accessor.AbstractSliderButtonAccessor", + "accessor.CameraAccessor" ], "injectors": { "defaultRequire": 1 }, + "overwrites": { + "requireAnnotations": true + }, "mixinextras": { "minVersion": "0.5.0" } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 07c52b5..8998d6a 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "id": "snapper", - "version": "${mod_version}", + "version": "${version}", "name": "Snapper", "description": "Screenshots, made simple.", "authors": [ @@ -9,6 +9,14 @@ "CallMeEcho", "hama" ], + "contributors": [ + "ChrysanthCow", + "moehreag", + "seriousfreezing", + "getchoo", + "vortixo", + "xaerfly" + ], "contact": { "homepage": "https://spiritstudios.dev", "sources": "https://github.com/SpiritGameStudios/Snapper", @@ -20,18 +28,21 @@ "entrypoints": { "client": [ "dev.spiritstudios.snapper.Snapper" + ], + "modmenu": [ + "dev.spiritstudios.snapper.compat.SnapperModMenu" ] }, - "accessWidener": "snapper.accesswidener", + "accessWidener": "snapper.classtweaker", "mixins": [ "snapper.mixins.json" ], "depends": { - "fabricloader": ">=${fabric_loader_version}", - "minecraft": "${minecraft_version}", + "fabricloader": ">=${loader_version}", + "minecraft": "1.21.10", "fabric-api": "*", "java": ">=21", - "specter-config": "*" + "greenhouseconfig": ">=3.0.0-" }, "custom": { "modmenu": { diff --git a/src/main/resources/snapper.accesswidener b/src/main/resources/snapper.accesswidener deleted file mode 100644 index 66736c7..0000000 --- a/src/main/resources/snapper.accesswidener +++ /dev/null @@ -1,2 +0,0 @@ -accessWidener v2 named -extendable method net/minecraft/client/gui/widget/EntryListWidget getEntryAtPosition (DD)Lnet/minecraft/client/gui/widget/EntryListWidget$Entry; \ No newline at end of file diff --git a/src/main/resources/snapper.classtweaker b/src/main/resources/snapper.classtweaker new file mode 100644 index 0000000..4c22e2e --- /dev/null +++ b/src/main/resources/snapper.classtweaker @@ -0,0 +1,4 @@ +classTweaker v1 named +extendable method net/minecraft/client/gui/components/AbstractSelectionList getEntryAtPosition (DD)Lnet/minecraft/client/gui/components/AbstractSelectionList$Entry; +accessible method net/minecraft/client/gui/components/AbstractSelectionList repositionEntries ()V +extendable method net/minecraft/client/gui/components/AbstractSelectionList repositionEntries ()V \ No newline at end of file