diff --git a/README.md b/README.md index 5a28977..7b3a3bb 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,10 @@ Screenshots, made simple -Available for Fabric - -Won't support Forge - Made by Spirit Studios +![Minimal Powered by OneConfig](https://polyfrost.org/media/branding/badges/badge_3.svg) + As seen on ModFest 1.21 Featured in BlanketCon '25 @@ -25,15 +23,10 @@ With Snapper, you can: - ~~Not having to install a mod from an EXE~~ ## ❓ FAQ -* Forge?
- No. Please do not ask us for a forge port. * Do you plan to / Could you add `X`?
If you want to suggest something, join the [Discord](https://discord.gg/TTmx7d2axf) or make a [GitHub issue](https://github.com/SpiritGameStudios/Snapper/issues). -* Can you port/backport to `X` version?
- Probably not. Multi-version projects are hard :< - * My screenshots aren't copying and my game is logging `Failed to get clipboard`.
The official Minecraft launcher doesn't have all of the necessary tools provided by a standard Java runtime. This may cause issues with copying for some users on Windows & Linux. Other launchers are not affected. @@ -47,4 +40,4 @@ Special thanks to our quality assurance team: --- -Snapper's image uploading is powered by the wonderful (and [open-source](https://github.com/axolotlclient)) [AxolotlClient](https://modrinth.com/mod/axolotlclient) and governed by their [terms of use](https://axolotlclient.com/terms). Agreement is opt-in and you may revoke it at any time. \ No newline at end of file +Snapper's image uploading is powered by the wonderful (and [open-source](https://github.com/axolotlclient)) [AxolotlClient](https://modrinth.com/mod/axolotlclient) and governed by their [terms of use](https://axolotlclient.com/terms). Agreement is opt-in and you may revoke it at any time. diff --git a/build.gradle.kts b/build.gradle.kts index 3a2525a..1fd66da 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,37 +1,84 @@ +@file:Suppress("UnstableApiUsage", "PropertyName") + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import dev.deftu.gradle.utils.GameSide +import dev.deftu.gradle.utils.version.MinecraftVersions + plugins { java - alias(libs.plugins.fabric.loom) - alias(libs.plugins.minotaur) + id("xyz.wagyourtail.jvmdowngrader") version "1.3.3" + kotlin("jvm") + id("dev.deftu.gradle.multiversion") // Applies preprocessing for multiple versions of Minecraft and/or multiple mod loaders. + id("dev.deftu.gradle.tools.configure") + id("dev.deftu.gradle.tools.repo") + id("dev.deftu.gradle.tools.resources") // Applies resource processing so that we can replace tokens, such as our mod name/version, in our resources. + id("dev.deftu.gradle.tools.bloom") // Applies the Bloom plugin, which allows us to replace tokens in our source files, such as being able to use `@MOD_VERSION` in our source files. + id("dev.deftu.gradle.tools.shadow") // Applies the Shadow plugin, which allows us to shade our dependencies into our mod JAR. This is NOT recommended for Fabric mods, but we have an *additional* configuration for those! + id("dev.deftu.gradle.tools.minecraft.loom") // Applies the Loom plugin, which automagically configures Essential's Architectury Loom plugin for you. + id("dev.deftu.gradle.tools.minecraft.releases") // Applies the Minecraft auto-releasing plugin, which allows you to automatically release your mod to CurseForge and Modrinth. } -class ModInfo { - val id = property("mod.id").toString() - val group = property("mod.group").toString() - val version = property("mod.version").toString() -} +toolkitLoomHelper { + useOneConfig { + version = "1.0.0-alpha.134" + loaderVersion = "1.1.0-alpha.48" + + usePolyMixin = true + polyMixinVersion = "0.8.4+build.6" -val mod = ModInfo() + applyLoaderTweaker = true -version = "${mod.version}+${libs.versions.minecraft.get()}" -group = mod.group + for (module in arrayOf("hud", "commands", "config", "config-impl", "events", "internal", "ui", "utils")) { + +module + } + } -base.archivesName = mod.id + useDevAuth("1.2.1") + useMixinExtras("0.4.1") + + // Turns off the server-side run configs, as we're building a client-sided mod. + disableRunConfigs(GameSide.SERVER) + + // Defines the name of the Mixin refmap, which is used to map the Mixin classes to the obfuscated Minecraft classes. + if (!mcData.isNeoForge) { + useMixinRefMap(modData.id) + } + + if (mcData.isForge) { + // Configures the Mixin tweaker if we are building for Forge. + useForgeMixin(modData.id) + } +} + +val mod_id = property("mod.id").toString() loom { - splitEnvironmentSourceSets() + if (mcData.version >= MinecraftVersions.VERSION_1_19 && mcData.isFabric) { + splitEnvironmentSourceSets() - mods.create(mod.id) { - sourceSet(sourceSets["main"]) - sourceSet(sourceSets["client"]) + mods.create(mod_id) { + sourceSet(sourceSets["main"]) + sourceSet(sourceSets["client"]) + } } +} - accessWidenerPath = file("src/main/resources/snapper.accesswidener") +if (mcData.version < MinecraftVersions.VERSION_1_19 || mcData.isForgeLike) { + sourceSets { + main { + val thisProject = if (mcData.version == MinecraftVersions.VERSION_1_21_5 && mcData.isFabric) rootProject else project + java { + srcDirs(thisProject.file("src/client/java"), thisProject.file("src/main/java")) + } + resources { + srcDirs(thisProject.file("src/client/resources"), thisProject.file("src/main/resources")) + } + } + } } repositories { - mavenCentral() maven("https://maven.spiritstudios.dev/releases/") - maven("https://maven.bawnorton.com/releases") maven("https://moehreag.duckdns.org/maven/releases") { content { includeGroup("io.github.axolotlclient.AxolotlClient") @@ -41,32 +88,64 @@ repositories { } dependencies { - minecraft(libs.minecraft) - mappings(variantOf(libs.yarn) { classifier("v2") }) - modImplementation(libs.fabric.loader) + fun Dependency?.applyExclusions() { + check(this != null && this is ModuleDependency) + exclude(module = "fabric-loader") + } - modImplementation(libs.fabric.api) + /** + * A pair of mappings used for the given environment. + * + * The first value is the mappings string, and the second value is whether these should be forced despite the requested configuration. + */ + val defaultMappings: Pair = when { + mcData.isLegacyFabric -> "net.legacyfabric:yarn:${mcData.dependencies.legacyFabric.legacyYarnVersion}" to true + mcData.isFabric -> "net.fabricmc:yarn:${mcData.dependencies.fabric.yarnVersion}:v2" to false + mcData.isForge && mcData.version <= MinecraftVersions.VERSION_1_15_2 -> mcData.dependencies.forge.mcpDependency to true + else -> "official" to false + } + val mappingsNotation = defaultMappings.first + + mappings(when(mappingsNotation) { + "official", "mojang", "mojmap" -> loom.officialMojangMappings() + + "official-like" -> { + if (mcData.version <= MinecraftVersions.VERSION_1_12_2) { + if (mcData.isForge) { + mcData.dependencies.forge.mcpDependency + } else { + repositories { + maven("https://raw.githubusercontent.com/BleachDev/cursed-mappings/main/") + } + + "net.legacyfabric:yarn:${mcData.version}+build.mcp" + } + } else loom.officialMojangMappings() + } - include(libs.bundles.specter) - modImplementation(libs.bundles.specter) + else -> mappingsNotation + }).applyExclusions() - implementation(libs.objc.bridge) + //TODO remove the above once https://github.com/Deftu/Gradle-Toolkit/pull/24 is merged - include("com.github.bawnorton.mixinsquared:mixinsquared-fabric:0.3.3") - implementation("com.github.bawnorton.mixinsquared:mixinsquared-fabric:0.3.3") - annotationProcessor("com.github.bawnorton.mixinsquared:mixinsquared-fabric:0.3.3") + 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() - ) +tasks { + fatJar { + sourceSets.findByName("client")?.let { clientSourceSet -> + from(sourceSets.getByName("main").output, clientSourceSet.output) + } + } + downgradeJar { + inputFile = this@tasks.named("fatJar").get().archiveFile + archiveClassifier = "downgraded-8-shaded" + } - inputs.properties(map) - filesMatching("fabric.mod.json") { expand(map) } + remapJar { + inputFile.set(shadeDowngradedApi.get().archiveFile) + dependsOn(shadeDowngradedApi) + } } java { @@ -82,7 +161,7 @@ tasks.withType { } tasks.jar { from("LICENSE") { rename { "${it}_${base.archivesName.get()}" } } } - +/* modrinth { token.set(System.getenv("MODRINTH_TOKEN")) projectId.set(mod.id) @@ -94,4 +173,6 @@ modrinth { dependencies { required.version("fabric-api", libs.versions.fabric.api.get()) } -} \ No newline at end of file +} + + */ \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 99b2004..98bc5df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,13 @@ org.gradle.jvmargs=-Xmx2G org.gradle.parallel=true +org.gradle.daemon=true +org.gradle.configureoncommand=true +org.gradle.parallel.threads=4 +loom.ignoreDependencyLoomVersionValidation=true +mod.name=Snapper mod.version = 1.1 mod.group = dev.spiritstudios mod.id = snapper -deps.minecraft=1.21.4 -deps.loader=0.16.5 +dgt.loom.mappings.use=false \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 813474a..1333876 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,26 +1,9 @@ [versions] -fabric_loom = "1.11-SNAPSHOT" -minotaur = "2.+" -minecraft = "1.21.4" -yarn = "1.21.4+build.3" - -fabric_loader = "0.16.14" -fabric_api = "0.119.3+1.21.4" - -specter = "1.1.3" +specter = "1.1.7" objc_bridge = "1.0.0" -[plugins] -fabric_loom = { id = "fabric-loom", version.ref = "fabric_loom" } -minotaur = { id = "com.modrinth.minotaur", version.ref = "minotaur" } - [libraries] -minecraft = { group = "mojang", name = "minecraft", version.ref = "minecraft" } -yarn = { group = "net.fabricmc", name = "yarn", version.ref = "yarn" } - -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 = "specter-config", version.ref = "specter" } specter_core = { group = "dev.spiritstudios.specter", name = "specter-core", version.ref = "specter" } diff --git a/root.gradle.kts b/root.gradle.kts new file mode 100644 index 0000000..fde10d9 --- /dev/null +++ b/root.gradle.kts @@ -0,0 +1,76 @@ +plugins { + id("dev.deftu.gradle.multiversion-root") +} + +preprocess { + // Adding new versions/loaders can be done like so: + // For each version, we add a new wrapper around the last from highest to lowest. + // Each mod loader needs to link up to the previous version's mod loader so that the mappings can be processed from the previous version. + // "1.12.2-forge"(11202, "srg") { + // "1.8.9-forge"(10809, "srg") + // } + + "1.21.5-fabric"(1_21_05, "yarn") { + "1.21.5-neoforge"(1_21_05, "srg") { + "1.21.4-neoforge"(1_21_04, "srg") { + "1.21.4-fabric"(1_21_04, "yarn") { + "1.21.3-fabric"(1_21_04, "yarn") { + "1.21.3-neoforge"(1_21_03, "srg") { + "1.21.2-neoforge"(1_21_02, "srg") { + "1.21.2-fabric"(1_21_02, "yarn") { + "1.21.1-fabric"(1_21_01, "yarn") { + "1.21.1-neoforge"(1_21_01, "srg") { + "1.20.6-neoforge"(1_20_06, "srg") { + "1.20.6-fabric"(1_20_06, "yarn") { + "1.20.4-fabric"(1_20_04, "yarn") { + "1.20.4-neoforge"(1_20_04, "srg") { + "1.20.4-forge"(1_20_04, "srg") { + "1.20.1-forge"(1_20_01, "srg") { + "1.20.1-fabric"(1_20_01, "yarn") { + "1.19.4-fabric"(1_19_04, "yarn") { + "1.19.4-forge"(1_19_04, "srg") { + "1.19.2-forge"(1_19_02, "srg") { + "1.19.2-fabric"(1_19_02, "yarn") { + "1.18.2-fabric"(1_18_02, "yarn") { + "1.18.2-forge"(1_18_02, "srg") { + "1.17.1-forge"(1_17_01, "srg") { + "1.17.1-fabric"(1_17_01, "yarn") { + "1.16.5-fabric"(1_16_05, "yarn"){ + "1.16.5-forge"(1_16_05, "srg") { + "1.12.2-forge"(1_12_02, "srg") { + "1.12.2-fabric"(1_12_02, "yarn") { + "1.8.9-fabric"(1_08_09, "yarn"){ + "1.8.9-forge"(1_08_09, "srg") + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + + strictExtraMappings.set(true) +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 64fdf10..44b096f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,60 @@ +@file:Suppress("PropertyName") + +import groovy.lang.MissingPropertyException + pluginManagement { repositories { - maven("https://maven.fabricmc.net/") - mavenCentral() + // Releases + maven("https://maven.deftu.dev/releases") + maven("https://maven.fabricmc.net") + maven("https://maven.architectury.dev/") + maven("https://maven.minecraftforge.net") + maven("https://repo.essential.gg/repository/maven-public") + maven("https://server.bbkr.space/artifactory/libs-release/") + maven("https://jitpack.io/") + + // Snapshots + maven("https://maven.deftu.dev/snapshots") + mavenLocal() + + // Default gradlePluginPortal() + mavenCentral() + } + + plugins { + kotlin("jvm") version("2.0.20") + id("dev.deftu.gradle.multiversion-root") version("2.49.0") } } + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.apache.commons:commons-compress:1.27.1") // Fixes old commons-compress version used by Gradle + } +} + +val projectName: String = extra["mod.name"]?.toString() + ?: throw MissingPropertyException("mod.name has not been set.") + +// Configures the root project Gradle name based on the value in `gradle.properties` +rootProject.name = projectName +rootProject.buildFileName = "root.gradle.kts" + +// Adds all of our build target versions to the classpath if we need to add version-specific code. +// Update this list if you want to remove/add a version and/or mod loader. +// The format is: version-modloader (f.ex: 1.8.9-forge, 1.17.1-fabric, etc) +// **REMEMBER TO ALSO UPDATE THE `root.gradle.kts` FILE WITH THE NEW VERSION(S). +listOf( + "1.21.5-neoforge", + "1.21.5-fabric" +).forEach { version -> + include(":$version") + project(":$version").apply { + projectDir = file("versions/$version") + buildFileName = "../../build.gradle.kts" + } +} \ No newline at end of file diff --git a/src/client/java/dev/spiritstudios/snapper/REMOVETHISAFTER1point21point5.java b/src/client/java/dev/spiritstudios/snapper/REMOVETHISAFTER1point21point5.java deleted file mode 100644 index 5269912..0000000 --- a/src/client/java/dev/spiritstudios/snapper/REMOVETHISAFTER1point21point5.java +++ /dev/null @@ -1,36 +0,0 @@ -package dev.spiritstudios.snapper; - -import com.bawnorton.mixinsquared.adjuster.tools.AdjustableAnnotationNode; -import com.bawnorton.mixinsquared.adjuster.tools.AdjustableInjectNode; -import com.bawnorton.mixinsquared.api.MixinAnnotationAdjuster; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import org.objectweb.asm.tree.MethodNode; -import org.spongepowered.asm.mixin.injection.Inject; - -import java.util.List; - -@Deprecated -// fixes a bug in specter -// specter is already on 1.21.5 and i really dont want to make a new version of specter- -// for an old version of mc. so you get this -// REMOVE THIS WHENEVER WE UPDATE TO 1.21.5 -public final class REMOVETHISAFTER1point21point5 implements MixinAnnotationAdjuster { - @Override - public AdjustableAnnotationNode adjust(List targetClassNames, String mixinClassName, MethodNode method, AdjustableAnnotationNode annotation) { - if (!mixinClassName.equals("dev.spiritstudios.specter.mixin.serialization.TranslatableTextContentMixin")) - return annotation; - if (!annotation.is(Inject.class)) return annotation; - - AdjustableInjectNode inject = annotation.as(AdjustableInjectNode.class); - - if (!inject.getMethod().contains("visit(Lnet/minecraft/text/StringVisitable$StyledVisitor;Lnet/minecraft/text/Style;)Ljava/util/Optional;") || !inject.getMethod().contains("visit(Lnet/minecraft/text/StringVisitable$Visitor;)Ljava/util/Optional;")) return annotation; - - return inject.withAt(at -> { - at.getFirst() - .withValue(ignored -> "INVOKE") - .withTarget(ignored -> "Ljava/util/List;iterator()Ljava/util/Iterator;"); - - return at; - }); - } -} diff --git a/src/client/java/dev/spiritstudios/snapper/Snapper.java b/src/client/java/dev/spiritstudios/snapper/Snapper.java index 60fe948..787f62c 100644 --- a/src/client/java/dev/spiritstudios/snapper/Snapper.java +++ b/src/client/java/dev/spiritstudios/snapper/Snapper.java @@ -3,34 +3,26 @@ import dev.spiritstudios.snapper.util.MacActions; import dev.spiritstudios.snapper.util.PlatformHelper; import dev.spiritstudios.snapper.util.WindowsActions; -import dev.spiritstudios.snapper.util.config.DirectoryConfigUtil; import dev.spiritstudios.snapper.util.uploading.ScreenshotUploading; -import dev.spiritstudios.specter.api.config.ConfigScreenWidgets; -import dev.spiritstudios.specter.api.config.ModMenuHelper; -import net.fabricmc.api.ClientModInitializer; -import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; -import net.fabricmc.loader.api.FabricLoader; import net.minecraft.util.Identifier; import net.minecraft.util.Util; +import org.polyfrost.oneconfig.api.event.v1.EventManager; +import org.polyfrost.oneconfig.api.event.v1.events.ShutdownEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.nio.file.Path; +import java.util.Optional; -public final class Snapper implements ClientModInitializer { - public static final String MODID = "snapper"; +public final class Snapper { + public static final String MODID = SnapperConstants.ID; public static final Logger LOGGER = LoggerFactory.getLogger(MODID); - public static final boolean IS_IRIS_INSTALLED = FabricLoader.getInstance().isModLoaded("iris"); + public static Optional IS_IRIS_INSTALLED = Optional.empty(); - @Override public void onInitializeClient() { - ConfigScreenWidgets.add(Path.class, DirectoryConfigUtil.PATH_WIDGET_FACTORY); SnapperKeybindings.init(); - ModMenuHelper.addConfig(Snapper.MODID, SnapperConfig.HOLDER.id()); - - ClientLifecycleEvents.CLIENT_STOPPING.register(client -> ScreenshotUploading.close()); + EventManager.register(ShutdownEvent.class, event -> ScreenshotUploading.close()); } public static Identifier id(String path) { diff --git a/src/client/java/dev/spiritstudios/snapper/SnapperConfig.java b/src/client/java/dev/spiritstudios/snapper/SnapperConfig.java index d54a4e5..4fd846c 100644 --- a/src/client/java/dev/spiritstudios/snapper/SnapperConfig.java +++ b/src/client/java/dev/spiritstudios/snapper/SnapperConfig.java @@ -1,47 +1,58 @@ package dev.spiritstudios.snapper; -import dev.spiritstudios.snapper.gui.screen.ScreenshotViewerScreen; +import dev.spiritstudios.snapper.gui.screen.ScreenshotScreen; import dev.spiritstudios.snapper.util.SnapperUtil; -import dev.spiritstudios.snapper.util.config.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 org.polyfrost.oneconfig.api.config.v1.Config; +import org.polyfrost.oneconfig.api.config.v1.annotations.Dropdown; +import org.polyfrost.oneconfig.api.config.v1.annotations.Include; +import org.polyfrost.oneconfig.api.config.v1.annotations.Switch; import java.nio.file.Path; -public final class SnapperConfig extends Config { - public static final ConfigHolder HOLDER = ConfigHolder.builder( - Snapper.id("snapper"), SnapperConfig.class - ).build(); - - public static final SnapperConfig INSTANCE = HOLDER.get(); - - public final Value copyTakenScreenshot = booleanValue(false) - .comment("Whether to copy screenshots to clipboard when taken.") - .build(); - - public final Value showSnapperTitleScreen = booleanValue(true) - .comment("Whether to show Snapper button on title screen.") - .build(); - - public final Value showSnapperGameMenu = booleanValue(true) - .comment("Whether to show Snapper button in game menu.") - .build(); - - public final Value viewMode = enumValue(ScreenshotViewerScreen.ViewMode.LIST, ScreenshotViewerScreen.ViewMode.class) - .comment("Whether to show screenshot menu with grid or list.") - .build(); - - public final Value termsAccepted = enumValue(AxolotlClientApi.TermsAcceptance.UNSET, AxolotlClientApi.TermsAcceptance.class) - .comment("Whether the terms of AxolotlClient have been accepted.") - .build(); - - public final Value useCustomScreenshotFolder = booleanValue(false) - .comment("Whether to use a custom screenshot folder instead of Minecraft's default") - .build(); - - public final Value customScreenshotFolder = value(Path.of(SnapperUtil.getOSUnifiedFolder().toString()), DirectoryConfigUtil.PATH_CODEC) - .comment("What folder to use if custom screenshot folders are enabled.") - .build(); -} \ No newline at end of file +public final class SnapperConfig extends Config { + + public static final SnapperConfig INSTANCE = new SnapperConfig(); + + @Switch( + title = "config.snapper.snapper.copyTakenScreenshot", + description = "config.snapper.snapper.copyTakenScreenshot.tooltip" + ) + public static boolean copyTakenScreenshot = false; + + @Switch( + title = "config.snapper.snapper.showSnapperTitleScreen", + description = "config.snapper.snapper.showSnapperTitleScreen.tooltip" + ) + public static boolean showSnapperTitleScreen = true; + + @Switch( + title = "config.snapper.snapper.showSnapperGameMenu", + description = "config.snapper.snapper.showSnapperGameMenu.tooltip" + ) + public static boolean showSnapperGameMenu = true; + + @Dropdown( + title = "config.snapper.snapper.viewMode", + description = "config.snapper.snapper.viewMode.tooltip" + ) + public static ScreenshotScreen.ViewMode viewMode = ScreenshotScreen.ViewMode.GRID; + + @Dropdown( + title = "config.snapper.snapper.termsAccepted", + description = "config.snapper.snapper.termsAccepted.tooltip" + ) + public static AxolotlClientApi.TermsAcceptance termsAccepted = AxolotlClientApi.TermsAcceptance.UNSET; + + @Switch( + title = "config.snapper.snapper.useCustomScreenshotFolder", + description = "config.snapper.snapper.useCustomScreenshotFolder.tooltip" + ) + public static boolean useCustomScreenshotFolder = false; + + @Include public static Path customScreenshotFolder = SnapperUtil.UNIFIED_FOLDER; //todo replace with a "file" option + + public SnapperConfig() { + super("snapper.json", "/assets/snapper/icon.png", "Snapper", Category.QOL); + } +} diff --git a/src/client/java/dev/spiritstudios/snapper/SnapperConstants.java b/src/client/java/dev/spiritstudios/snapper/SnapperConstants.java new file mode 100644 index 0000000..4a2c65b --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/SnapperConstants.java @@ -0,0 +1,10 @@ +package dev.spiritstudios.snapper; + +public class SnapperConstants { + + // Sets the variables from `gradle.properties`. Depends on the `bloom` DGT plugin. + public static final String ID = "@MOD_ID@"; + public static final String NAME = "@MOD_NAME@"; + public static final String VERSION = "@MOD_VERSION@"; + +} \ No newline at end of file diff --git a/src/client/java/dev/spiritstudios/snapper/SnapperEntrypoint.java b/src/client/java/dev/spiritstudios/snapper/SnapperEntrypoint.java new file mode 100644 index 0000000..da8c534 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/SnapperEntrypoint.java @@ -0,0 +1,79 @@ +package dev.spiritstudios.snapper; + +//#if FABRIC +import net.fabricmc.api.ClientModInitializer; +//#elseif FORGE +//#if MC >= 1.16.5 +//$$ import net.minecraftforge.eventbus.api.IEventBus; +//$$ import net.minecraftforge.fml.common.Mod; +//$$ import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; +//$$ import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +//#else +//$$ import net.minecraftforge.fml.common.Mod; +//$$ import net.minecraftforge.fml.common.event.FMLInitializationEvent; +//#endif +//#elseif NEOFORGE +//$$ import net.neoforged.bus.api.IEventBus; +//$$ import net.neoforged.fml.common.Mod; +//$$ import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; +//#endif + +//#if FORGE-LIKE +//#if MC >= 1.16.5 +//$$ @Mod(SnapperConstants.ID) +//#else +//$$ @Mod(modid = SnapperConstants.ID, version = SnapperConstants.VERSION) +//#endif +//#endif +public class SnapperEntrypoint + //#if FABRIC + implements ClientModInitializer + //#endif +{ + + //#if FORGE && MC >= 1.16.5 + //$$ public SnapperEntrypoint() { + //$$ setupForgeEvents(FMLJavaModLoadingContext.get().getModEventBus()); + //$$ } + //#elseif NEOFORGE + //$$ public SnapperEntrypoint(IEventBus modEventBus) { + //$$ setupForgeEvents(modEventBus); + //$$ } + //#endif + + //#if FABRIC + @Override + //#elseif FORGE && MC <= 1.12.2 + //$$ @Mod.EventHandler + //#endif + public void onInitializeClient( + //#if FORGE-LIKE + //#if MC >= 1.16.5 + //$$ FMLClientSetupEvent event + //#else + //$$ FMLInitializationEvent event + //#endif + //#endif + ) { + //#if FORGE && MC <= 1.12.2 + //$$ if (!event + //#if MC <= 1.8.9 + //$$ .side.isClient + //#else + //$$ .getSide().isClient() + //#endif + //$$ ) { + //$$ return; + //$$ } + //#endif + + new Snapper().onInitializeClient(); + } + + //#if FORGE-LIKE && MC >= 1.16.5 + //$$ private void setupForgeEvents(IEventBus modEventBus) { + //$$ modEventBus.addListener(this::onInitializeClient); + //$$ } + //#endif + +} \ No newline at end of file diff --git a/src/client/java/dev/spiritstudios/snapper/SnapperKeybindings.java b/src/client/java/dev/spiritstudios/snapper/SnapperKeybindings.java index 295dcd3..eccacfc 100644 --- a/src/client/java/dev/spiritstudios/snapper/SnapperKeybindings.java +++ b/src/client/java/dev/spiritstudios/snapper/SnapperKeybindings.java @@ -1,81 +1,95 @@ package dev.spiritstudios.snapper; +import dev.deftu.omnicore.client.OmniChat; +import dev.deftu.omnicore.client.OmniScreen; +import dev.deftu.omnicore.client.keybindings.ManagedKeyBinding; +import dev.deftu.omnicore.client.keybindings.OmniKeyBinding; +import dev.deftu.omnicore.common.OmniLoader; +import dev.deftu.textile.minecraft.MCTextHolder; import dev.spiritstudios.snapper.gui.screen.ScreenshotScreen; import dev.spiritstudios.snapper.gui.screen.ScreenshotViewerScreen; +import dev.spiritstudios.snapper.util.DynamicTexture; import dev.spiritstudios.snapper.util.ScreenshotActions; -import dev.spiritstudios.snapper.util.ScreenshotImage; -import dev.spiritstudios.specter.api.core.util.ClientKeybindEvents; -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 org.lwjgl.glfw.GLFW; +import org.polyfrost.oneconfig.api.event.v1.EventManager; +import org.polyfrost.oneconfig.api.event.v1.events.KeyInputEvent; import java.nio.file.Path; import java.util.List; +import java.util.Optional; public final class SnapperKeybindings { - public static final KeyBinding PANORAMA_KEY = new KeyBinding( + public static final ManagedKeyBinding PANORAMA_KEY = OmniKeyBinding.create( "key.snapper.panorama", - GLFW.GLFW_KEY_F8, - "key.categories.snapper" + "key.categories.snapper", + GLFW.GLFW_KEY_F8 ); - public static final KeyBinding RECENT_SCREENSHOT_KEY = new KeyBinding( + public static final ManagedKeyBinding RECENT_SCREENSHOT_KEY = OmniKeyBinding.create( "key.snapper.recent", - GLFW.GLFW_KEY_O, - "key.categories.snapper" + "key.categories.snapper", + GLFW.GLFW_KEY_O ); - public static final KeyBinding SCREENSHOT_MENU_KEY = new KeyBinding( + public static final ManagedKeyBinding SCREENSHOT_MENU_KEY = OmniKeyBinding.create( "key.snapper.screenshot_menu", - GLFW.GLFW_KEY_V, - "key.categories.snapper" + "key.categories.snapper", + GLFW.GLFW_KEY_V ); public static void init() { - KeyBindingHelper.registerKeyBinding(PANORAMA_KEY); - 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); + SCREENSHOT_MENU_KEY.register(); + PANORAMA_KEY.register(); + RECENT_SCREENSHOT_KEY.register(); + EventManager.register(KeyInputEvent.class, (event) -> { //todo replace with omnicore keybind methods once that gets pushed to oneconfig + if (SCREENSHOT_MENU_KEY.consume()) { + OmniScreen.setCurrentScreen(new ScreenshotScreen(OmniScreen.getCurrentScreen())); + } + if (PANORAMA_KEY.consume()) { + takePanorama(MinecraftClient.getInstance()); + } + if (RECENT_SCREENSHOT_KEY.consume()) { + openRecentScreenshot(MinecraftClient.getInstance()); + } + }); } private static void takePanorama(MinecraftClient client) { if (client.player == null) return; - if (Snapper.IS_IRIS_INSTALLED) { - client.player.sendMessage(Text.translatable("text.snapper.panorama_failure_iris"), true); + if (Snapper.IS_IRIS_INSTALLED.orElseGet(() -> { Snapper.IS_IRIS_INSTALLED = Optional.of(OmniLoader.isModLoaded("iris")); return Snapper.IS_IRIS_INSTALLED.get(); })) { // TODO migrate all usages of "displayClientMessage" to use action bar/overlay + OmniChat.displayClientMessage(MCTextHolder.convertFromVanilla(Text.translatable("text.snapper.panorama_failure_iris"))); return; } client.takePanorama(client.runDirectory, 1024, 1024); - client.player.sendMessage(Text.translatable( + OmniChat.displayClientMessage(MCTextHolder.convertFromVanilla(Text.translatable( "text.snapper.panorama_success", - SCREENSHOT_MENU_KEY.getBoundKeyLocalizedText() - ), true); + SCREENSHOT_MENU_KEY.getVanillaKeyBinding().getBoundKeyLocalizedText() + ))); } private static void openRecentScreenshot(MinecraftClient client) { - List screenshots = ScreenshotActions.getScreenshots(client); + List screenshots = ScreenshotActions.getScreenshots(); if (screenshots.isEmpty()) { if (client.player != null) - client.player.sendMessage(Text.translatable("text.snapper.screenshot_not_exists"), true); + OmniChat.displayClientMessage(MCTextHolder.convertFromVanilla(Text.translatable("text.snapper.screenshot_not_exists"))); return; } Path latestPath = screenshots.getFirst(); - ScreenshotImage.createScreenshot(client.getTextureManager(), latestPath) + DynamicTexture.createScreenshot(client.getTextureManager(), latestPath) .ifPresentOrElse( - image -> client.setScreen(new ScreenshotViewerScreen( - image, - latestPath, - client.currentScreen - )), + image -> { + client.setScreen(new ScreenshotViewerScreen( + image, + latestPath, + client.currentScreen + )); + image.load(); + }, () -> { if (client.player != null) client.player.sendMessage(Text.translatable("text.snapper.screenshot_open_failure"), true); 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 c20049d..2ba7d55 100644 --- a/src/client/java/dev/spiritstudios/snapper/gui/overlay/ExternalDialogOverlay.java +++ b/src/client/java/dev/spiritstudios/snapper/gui/overlay/ExternalDialogOverlay.java @@ -23,11 +23,9 @@ public boolean pausesGame() { public void render(DrawContext context, int mouseX, int mouseY, float delta) { if (this.client.currentScreen != null) { this.client.currentScreen.render(context, mouseX, mouseY, delta); - context.draw(); } this.client.gameRenderer.renderBlur(); - this.client.getFramebuffer().beginWrite(false); context.drawTexture( RenderLayer::getGuiTexturedOverlay, @@ -48,7 +46,6 @@ public void render(DrawContext context, int mouseX, int mouseY, float delta) { if (InputUtil.isKeyPressed(client.getWindow().getHandle(), InputUtil.GLFW_KEY_ESCAPE)) close(); } - public void close() { this.client.setOverlay(null); } 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..704a380 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/gui/screen/ConfigScreen.java @@ -0,0 +1,86 @@ +package dev.spiritstudios.snapper.gui.screen; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import dev.spiritstudios.snapper.Snapper; +import dev.spiritstudios.snapper.gui.widget.OptionsScrollableWidget; +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.ButtonWidget; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.screen.ScreenTexts; +import net.minecraft.text.Text; +import org.polyfrost.oneconfig.api.config.v1.Config; +import org.polyfrost.oneconfig.api.config.v1.Property; + +public class ConfigScreen extends Screen { + protected final Config config; + protected final Screen parent; + + public ConfigScreen(Config config, Screen parent) { + super(Text.translatable("config.snapper.title")); + this.config = config; + this.parent = parent; + } + + + @Override + protected void init() { + super.init(); + Objects.requireNonNull(this.client); + + OptionsScrollableWidget scrollableWidget = new OptionsScrollableWidget(this.client, this.width, this.height - 64, 32, 25); + + List options = new ArrayList<>(); + config.getTree().map.forEach((key, value) -> { + if (value instanceof Property) { + Property property = (Property) value; + Function, ? extends ClickableWidget> factory = ConfigScreenWidgets.getWidgetFactory(property); + if (factory == null) { + Snapper.LOGGER.warn("No widget factory found for {}", property.type.getSimpleName()); + return; + } + + ClickableWidget widget = factory.apply(property); + if (widget == null) + throw new IllegalStateException("Widget factory returned null for %s".formatted(property.type.getSimpleName())); + + widget.setWidth(0); + widget.setHeight(20); + + Text tooltip = Text.translatableWithFallback("%s.tooltip".formatted(property.getTitle()), ""); + if (!tooltip.getString().isEmpty()) widget.setTooltip(Tooltip.of(tooltip)); + + options.add(widget); + } + }); + + scrollableWidget.addOptions(options); + this.addDrawableChild(scrollableWidget); + this.addDrawableChild(new ButtonWidget.Builder(ScreenTexts.DONE, button -> close()).dimensions(this.width / 2 - 100, this.height - 27, 200, 20).build()); + } + + @Override + public void close() { + save(); + + Objects.requireNonNull(this.client); + this.client.setScreen(this.parent); + } + + public void save() { + config.save(); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + this.renderBackground(context, mouseX, mouseY, delta); + + super.render(context, mouseX, mouseY, delta); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 0xFFFFFF); + } +} \ No newline at end of file diff --git a/src/client/java/dev/spiritstudios/snapper/gui/screen/ConfigScreenWidgets.java b/src/client/java/dev/spiritstudios/snapper/gui/screen/ConfigScreenWidgets.java new file mode 100644 index 0000000..be79dd2 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/gui/screen/ConfigScreenWidgets.java @@ -0,0 +1,118 @@ +package dev.spiritstudios.snapper.gui.screen; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import dev.spiritstudios.snapper.gui.widget.SpecterButtonWidget; +import dev.spiritstudios.snapper.gui.widget.SpecterSliderWidget; +import dev.spiritstudios.snapper.util.PatternMap; +import org.jetbrains.annotations.ApiStatus; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.screen.ScreenTexts; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.polyfrost.oneconfig.api.config.v1.Property; + + +@SuppressWarnings("unchecked") +public final class ConfigScreenWidgets { + private static final PatternMap, ? extends ClickableWidget>> widgetFactories = new PatternMap<>(); + private static final Function, ? extends ClickableWidget> BOOLEAN_WIDGET_FACTORY = (configValue) -> { + Property value = (Property) configValue; + + return SpecterButtonWidget.builder( + () -> Text.translatable(value.getTitle()).append(": ").append(ScreenTexts.onOrOff(value.get())), + button -> value.set(!value.get()) + ).build(); + }; + private static final Function, ? extends ClickableWidget> INTEGER_WIDGET_FACTORY = (configValue) -> { + Property value = (Property) configValue; + + return SpecterSliderWidget.builder(value.get()) + .message((val) -> Text.translatable(value.getTitle()).append(String.format(": %.0f", val))) + .range(value.getMetadata("min"), value.getMetadata("max")) + .step((float) value.getMetadata("step") == 0 ? 1 : value.getMetadata("step")) + .onValueChanged((val) -> value.set(val.intValue())) + .build(); + }; + private static final Function, ? extends ClickableWidget> DOUBLE_WIDGET_FACTORY = (configValue) -> { + Property value = (Property) configValue; + + return SpecterSliderWidget.builder(value.get()) + .message((val) -> Text.translatable(value.getTitle()).append(String.format(": %.2f", val))) + .range(value.getMetadata("min"), value.getMetadata("max")) + .step((float) value.getMetadata("step") == 0 ? 1 : value.getMetadata("step")) + .onValueChanged(value::set) + .build(); + }; + private static final Function, ? extends ClickableWidget> FLOAT_WIDGET_FACTORY = (configValue) -> { + Property value = (Property) configValue; + + return SpecterSliderWidget.builder(value.get()) + .message((val) -> Text.translatable(configValue.getTitle()).append(String.format(": %.1f", val))) + .range(value.getMetadata("min"), value.getMetadata("max")) + .step((float) value.getMetadata("step") == 0 ? 1 : value.getMetadata("step")) + .onValueChanged((val) -> value.set(val.floatValue())) + .build(); + }; + private static final Function, ? extends ClickableWidget> STRING_WIDGET_FACTORY = (configValue) -> { + Property value = (Property) configValue; + + TextFieldWidget widget = new TextFieldWidget(MinecraftClient.getInstance().textRenderer, 0, 0, 0, 0, Text.of(value.get())); + widget.setPlaceholder(Text.translatableWithFallback("%s.placeholder".formatted(configValue.getTitle()), "").formatted(Formatting.DARK_GRAY)); + + widget.setText(value.get()); + widget.setChangedListener(value::set); + widget.setSelectionEnd(0); + widget.setSelectionStart(0); + + return widget; + }; + private static final Function, ? extends ClickableWidget> ENUM_WIDGET_FACTORY = (configValue) -> { + Property> value = (Property>) configValue; + + List> enumValues = Arrays.stream(configValue.type.getEnumConstants()) + .filter(val -> val instanceof Enum) + .map(val -> (Enum) val) + .collect(Collectors.toList()); + + if (enumValues.isEmpty()) throw new IllegalArgumentException("Enum values cannot be null"); + + return SpecterButtonWidget.builder( + () -> Text.translatable(configValue.getTitle()).append(": ").append(Text.translatable("%s.%s".formatted(configValue.getTitle(), value.get().toString().toLowerCase()))), + button -> { + Enum current = value.get(); + int index = enumValues.indexOf(current); + value.set(enumValues.get((index + 1) % enumValues.size())); + } + ).build(); + }; + + private ConfigScreenWidgets() { + } + + public static void add(Class clazz, Function, ? extends ClickableWidget> factory) { + widgetFactories.put(clazz, factory); + } + + @ApiStatus.Internal + public static Function, ? extends ClickableWidget> getWidgetFactory(Property value) { + // We are using a switch instead of just adding to our map for 2 reasons: + // 1. It's (usually) faster than a map lookup, as most of the time the value will be one of these types + // 2. It lets us handle the lowercased names of primitive types, which are different Class<> instances because reasons + return switch (value.get()) { + case Boolean ignored -> BOOLEAN_WIDGET_FACTORY; + case Integer ignored -> INTEGER_WIDGET_FACTORY; + case Double ignored -> DOUBLE_WIDGET_FACTORY; + case Float ignored -> FLOAT_WIDGET_FACTORY; + case String ignored -> STRING_WIDGET_FACTORY; + case Enum ignored -> ENUM_WIDGET_FACTORY; + case null, default -> widgetFactories.get(value.type); + }; + } +} \ No newline at end of file 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 b90ac70..ae35d6c 100644 --- a/src/client/java/dev/spiritstudios/snapper/gui/screen/PanoramaViewerScreen.java +++ b/src/client/java/dev/spiritstudios/snapper/gui/screen/PanoramaViewerScreen.java @@ -1,9 +1,10 @@ package dev.spiritstudios.snapper.gui.screen; import dev.spiritstudios.snapper.Snapper; +import dev.spiritstudios.snapper.util.DynamicTexture; import dev.spiritstudios.snapper.util.SafeFiles; -import dev.spiritstudios.snapper.util.ScreenshotImage; 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; @@ -11,33 +12,56 @@ import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.screen.ScreenTexts; import net.minecraft.text.Text; -import net.minecraft.util.Identifier; import net.minecraft.util.Util; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.stream.Stream; public class PanoramaViewerScreen extends Screen { - protected static final CubeMapRenderer PANORAMA_RENDERER = new CubeMapRenderer(Identifier.ofVanilla("screenshots/panorama/panorama")); + protected static final CubeMapRenderer PANORAMA_RENDERER = new CubeMapRenderer(Snapper.id("screenshots/panorama/panorama")); - protected static final RotatingCubeMapRenderer PANORAMA_RENDERER_CUBE = new RotatingCubeMapRenderer(PANORAMA_RENDERER); + protected static final RotatingCubeMapRenderer ROTATING_PANORAMA_RENDERER = new RotatingCubeMapRenderer(PANORAMA_RENDERER); private final String title; private final Screen parent; + private final List images = new ArrayList<>(); + protected PanoramaViewerScreen(String title, Screen parent) { super(Text.translatable("menu.snapper.viewer_menu")); this.title = title; this.parent = parent; + this.client = MinecraftClient.getInstance(); + assert this.client != null; + + List facePaths = this.getImagePaths(); + + if (facePaths == null) { + Snapper.LOGGER.error("No panorama found"); + close(); + return; + } + + for (Path path : facePaths) { + DynamicTexture.createPanoramaFace(this.client.getTextureManager(), path) + .ifPresent(screenshotImage -> { + images.add(screenshotImage); + screenshotImage + .load() + .thenAccept(ignored -> screenshotImage.enableFiltering()); + }); + } } - @Nullable - private List getImagePaths() { + + private @Nullable @Unmodifiable List getImagePaths() { Objects.requireNonNull(this.client); Path panoramaDir = SnapperUtil.getConfiguredScreenshotDirectory().resolve("panorama"); @@ -61,6 +85,10 @@ private List getImagePaths() { public void close() { Objects.requireNonNull(this.client); + for (DynamicTexture image : images) { + image.close(); + } + client.setScreen(this.parent); } @@ -70,12 +98,6 @@ protected void init() { Path panoramaPath = Path.of(client.runDirectory.getPath(), "screenshots", "panorama"); addDrawableChild(ButtonWidget.builder(Text.translatable("button.snapper.folder"), button -> { - if (!SafeFiles.createDirectories(panoramaPath)) { - Snapper.LOGGER.error("Failed to create directory \"{}\"", panoramaPath); - close(); - return; - } - Util.getOperatingSystem().open(panoramaPath); }).dimensions(width / 2 - 150 - 4, height - 32, 150, 20).build()); @@ -83,23 +105,11 @@ protected void init() { ScreenTexts.DONE, button -> this.close() ).dimensions(width / 2 + 4, height - 32, 150, 20).build()); - - List panorama = this.getImagePaths(); - if (panorama == null) { - Snapper.LOGGER.error("No panorama found"); - close(); - return; - } - - for (Path path : panorama) { - ScreenshotImage.createPanoramaFace(this.client.getTextureManager(), path) - .ifPresent(ScreenshotImage::enableFiltering); - } } @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { - PANORAMA_RENDERER_CUBE.render(context, this.width, this.height, 1.0F, delta); + ROTATING_PANORAMA_RENDERER.render(context, this.width, this.height, 1.0F, delta); context.drawCenteredTextWithShadow( this.textRenderer, 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 a01dc71..4fde871 100644 --- a/src/client/java/dev/spiritstudios/snapper/gui/screen/PrivacyNoticeScreen.java +++ b/src/client/java/dev/spiritstudios/snapper/gui/screen/PrivacyNoticeScreen.java @@ -92,15 +92,15 @@ private void addButtons(int y) { addDrawableChild(ButtonWidget.builder(Text.translatable("snapper.privacy_notice.accept"), buttonWidget -> { client.setScreen(parent); - SnapperConfig.INSTANCE.termsAccepted.set(AxolotlClientApi.TermsAcceptance.ACCEPTED); - SnapperConfig.HOLDER.save(); + SnapperConfig.termsAccepted = AxolotlClientApi.TermsAcceptance.ACCEPTED; + SnapperConfig.INSTANCE.save(); accepted.accept(true); }).dimensions(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(); + SnapperConfig.termsAccepted = AxolotlClientApi.TermsAcceptance.DENIED; + SnapperConfig.INSTANCE.save(); accepted.accept(false); }).dimensions(width / 2 - (buttonWidth / 2) + buttonWidth + 5, y, buttonWidth, 20).build()); } diff --git a/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotScreen.java b/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotScreen.java index 9c02b51..fdb11a4 100644 --- a/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotScreen.java +++ b/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotScreen.java @@ -6,12 +6,14 @@ 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.RootConfigScreen; -import net.minecraft.client.MinecraftClient; 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.*; +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; @@ -22,16 +24,20 @@ import java.nio.file.Path; -import static dev.spiritstudios.snapper.Snapper.MODID; - public class ScreenshotScreen extends Screen { - private static final Identifier PANORAMA_BUTTON_ICON = Identifier.of(MODID, "screenshots/panorama"); - private static final Identifier PANORAMA_BUTTON_DISABLED_ICON = Identifier.of(MODID, "screenshots/panorama_disabled"); + 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 static final Identifier SETTINGS_ICON = Identifier.of(MODID, "screenshots/settings"); - private static Identifier VIEW_MODE_ICON; private final Screen parent; - ScreenshotListWidget screenshotList; + private final boolean isOffline; + + private ScreenshotListWidget screenshotList; private ButtonWidget deleteButton; private ButtonWidget renameButton; private ButtonWidget viewButton; @@ -39,17 +45,15 @@ public class ScreenshotScreen extends Screen { private ButtonWidget openButton; private ButtonWidget uploadButton; private TextIconButtonWidget viewModeButton; - private ScreenshotListWidget.@Nullable ScreenshotEntry selectedScreenshot = null; + private @Nullable ScreenshotListWidget.ScreenshotEntry selectedScreenshot = null; private boolean showGrid; - private final boolean isOffline; public ScreenshotScreen(Screen parent) { super(Text.translatable("menu.snapper.screenshot_menu")); this.parent = parent; - this.showGrid = SnapperConfig.INSTANCE.viewMode.get().equals(ScreenshotViewerScreen.ViewMode.GRID); + this.showGrid = SnapperConfig.viewMode.equals(ViewMode.GRID); this.isOffline = SnapperUtil.isOfflineAccount(); - VIEW_MODE_ICON = showGrid ? Identifier.of(MODID, "screenshots/show_list") : Identifier.of(MODID, "screenshots/show_grid"); } @Override @@ -76,8 +80,9 @@ protected void init() { this.openButton = addDrawableChild(ButtonWidget.builder( Text.translatable("button.snapper.open"), button -> { - if (selectedScreenshot != null) - Util.getOperatingSystem().open(selectedScreenshot.path); + if (selectedScreenshot != null) { + Util.getOperatingSystem().open(selectedScreenshot.icon.getPath()); + } } ).width(secondRowButtonWidth).build()); @@ -91,36 +96,40 @@ protected void init() { this.deleteButton = addDrawableChild(ButtonWidget.builder( Text.translatable("button.snapper.delete"), button -> { - if (selectedScreenshot != null) - ScreenshotActions.deleteScreenshot(selectedScreenshot.path, this); + 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 RenameScreenshotScreen(this.selectedScreenshot.path, this)); + if (this.selectedScreenshot != null) { + client.setScreen(new RenameScreenshotScreen(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.path); + 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.path, - selectedScreenshot.screenParent - )); + if (selectedScreenshot != null) { + this.client.setScreen(new ScreenshotViewerScreen( + selectedScreenshot.icon, + selectedScreenshot.icon.getPath(), + selectedScreenshot.screenParent + )); + } } ).width(firstRowButtonWidth).build()); @@ -128,7 +137,7 @@ protected void init() { if (selectedScreenshot == null) return; button.active = false; - ScreenshotUploading.upload(selectedScreenshot.path) + ScreenshotUploading.upload(selectedScreenshot.icon.getPath()) .thenRun(() -> button.active = true); }).width(firstRowButtonWidth).build()); @@ -136,7 +145,8 @@ protected void init() { this.uploadButton.setTooltip(Tooltip.of(Text.translatable("button.snapper.upload.tooltip"))); } - DirectionalLayoutWidget verticalButtonLayout = DirectionalLayoutWidget.vertical().spacing(4); + DirectionalLayoutWidget verticalButtonLayout = DirectionalLayoutWidget.vertical() + .spacing(4); AxisGridWidget firstRowWidget = verticalButtonLayout.add(new AxisGridWidget( 308, @@ -166,7 +176,7 @@ protected void init() { TextIconButtonWidget settingsButton = addDrawableChild(TextIconButtonWidget.builder( Text.translatable("config.snapper.snapper.title"), button -> this.client.setScreen( - new RootConfigScreen(SnapperConfig.HOLDER, new ScreenshotScreen(this.parent))), + new ConfigScreen(SnapperConfig.INSTANCE, new ScreenshotScreen(this.parent))), true ).width(20).texture(SETTINGS_ICON, 15, 15).build()); @@ -177,7 +187,7 @@ protected void init() { Text.translatable("config.snapper.snapper.viewMode"), button -> this.toggleGrid(), true - ).width(20).texture(VIEW_MODE_ICON, 15, 15).build()); + ).width(20).texture(showGrid ? VIEW_MODE_ICON_LIST : VIEW_MODE_ICON_GRID, 15, 15).build()); viewModeButton.setPosition(width / 2 - 178, height - 56); @@ -216,14 +226,13 @@ public void toggleGrid() { screenshotList.toggleGrid(); screenshotList.refreshScroll(); this.showGrid = !this.showGrid; - VIEW_MODE_ICON = showGrid ? Identifier.of(MODID, "screenshots/show_list") : Identifier.of(MODID, "screenshots/show_grid"); remove(this.viewModeButton); this.viewModeButton = addDrawableChild(TextIconButtonWidget.builder( Text.translatable("config.snapper.snapper.viewMode"), button -> this.toggleGrid(), true - ).width(20).texture(VIEW_MODE_ICON, 15, 15).build()); + ).width(20).texture(showGrid ? VIEW_MODE_ICON_LIST : VIEW_MODE_ICON_GRID, 15, 15).build()); viewModeButton.setPosition(width / 2 - 178, height - 56); } @@ -233,7 +242,6 @@ public boolean keyPressed(int keyCode, int scanCode, int modifiers) { if (client == null) return false; if (keyCode == GLFW.GLFW_KEY_F5) { - client.setScreen(new ScreenshotScreen(this.parent)); return true; } @@ -241,20 +249,21 @@ public boolean keyPressed(int keyCode, int scanCode, int modifiers) { if (selectedScreenshot == null) return false; if ((modifiers & GLFW.GLFW_MOD_CONTROL) != 0 && keyCode == InputUtil.GLFW_KEY_C) { - Snapper.getPlatformHelper().copyScreenshot(selectedScreenshot.path); + Snapper.getPlatformHelper().copyScreenshot(selectedScreenshot.icon.getPath()); return true; } if (keyCode == GLFW.GLFW_KEY_ENTER) { - client.setScreen(new ScreenshotViewerScreen(selectedScreenshot.icon, selectedScreenshot.path, this)); - } + client.setScreen(new ScreenshotViewerScreen(selectedScreenshot.icon, selectedScreenshot.icon.getPath(), this)); + return true; + } return false; } @Override public void close() { - SnapperConfig.HOLDER.save(); + SnapperConfig.INSTANCE.save(); super.close(); } @@ -263,4 +272,9 @@ 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, 0xffffff); } + + 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 c506922..494ca59 100644 --- a/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotViewerScreen.java +++ b/src/client/java/dev/spiritstudios/snapper/gui/screen/ScreenshotViewerScreen.java @@ -1,12 +1,11 @@ package dev.spiritstudios.snapper.gui.screen; -import com.mojang.blaze3d.systems.RenderSystem; +import dev.deftu.omnicore.common.OmniLoader; import dev.spiritstudios.snapper.Snapper; +import dev.spiritstudios.snapper.util.DynamicTexture; import dev.spiritstudios.snapper.util.ScreenshotActions; -import dev.spiritstudios.snapper.util.ScreenshotImage; 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.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; @@ -30,14 +29,16 @@ 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 final MinecraftClient client = MinecraftClient.getInstance(); - private final ScreenshotImage icon; + private final DynamicTexture image; private final String title; private final int imageWidth; private final int imageHeight; @@ -47,11 +48,11 @@ public class ScreenshotViewerScreen extends Screen { private final int screenshotIndex; private final Path iconPath; - public ScreenshotViewerScreen(ScreenshotImage icon, Path screenshot, Screen parent) { + public ScreenshotViewerScreen(DynamicTexture icon, Path screenshot, Screen parent) { this(icon, screenshot, parent, null); } - public ScreenshotViewerScreen(ScreenshotImage icon, Path iconPath, Screen parent, @Nullable List screenshots) { + public ScreenshotViewerScreen(DynamicTexture icon, Path iconPath, Screen parent, @Nullable List screenshots) { super(Text.translatable("menu.snapper.viewer_menu")); this.parent = parent; this.iconPath = iconPath; @@ -65,7 +66,7 @@ public ScreenshotViewerScreen(ScreenshotImage icon, Path iconPath, Screen parent this.client.setScreen(parent); } - this.icon = icon; + this.image = icon; this.title = iconPath.getFileName().toString(); this.imageWidth = image != null ? image.getWidth() : 0; @@ -77,12 +78,7 @@ public ScreenshotViewerScreen(ScreenshotImage icon, Path iconPath, Screen parent this.screenshotIndex = this.screenshots != null ? this.screenshots.indexOf(this.screenshot) : -1; } - public enum ViewMode { - LIST, - GRID - } - - @Override + @Override public void close() { this.client.setScreen(this.parent); } @@ -196,7 +192,7 @@ public void render(DrawContext context, int mouseX, int mouseY, float delta) { context.drawTexture( RenderLayer::getGuiTextured, - this.icon.getTextureId(), + this.image.getTextureId(), (this.width / 2) - (finalWidth / 2), this.height - 68 - finalHeight, 0, 0, finalWidth, finalHeight, @@ -213,7 +209,7 @@ public void render(DrawContext context, int mouseX, int mouseY, float delta) { ); } - if (FabricLoader.getInstance().isDevelopmentEnvironment()) renderDebugInfo(context); + if (OmniLoader.isDevelopment()) renderDebugInfo(context); } private void renderDebugInfo(DrawContext context) { @@ -260,10 +256,14 @@ private void drawMenuBackground(DrawContext context) { this.client.world == null ? MENU_DECOR_BACKGROUND_TEXTURE : INWORLD_MENU_DECOR_BACKGROUND_TEXTURE, - width, height - 68 - 48, - 0, 0, - 32, 32, - 0, 48 + 0, + 48, + 0, + 0, + width, + height - 68 - 48, + 32, + 32 ); } @@ -307,13 +307,17 @@ public boolean keyPressed(int keyCode, int scanCode, int modifiers) { }; if (imagePath == null) return super.keyPressed(keyCode, scanCode, modifiers); - - ScreenshotImage.createScreenshot(client.getTextureManager(), imagePath) - .ifPresent(image -> client.setScreen(new ScreenshotViewerScreen( - image, imagePath, - this.parent, - this.screenshots - ))); + 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/widget/FolderSelectWidget.java b/src/client/java/dev/spiritstudios/snapper/gui/widget/FolderSelectWidget.java index bc509cc..0b108fd 100644 --- a/src/client/java/dev/spiritstudios/snapper/gui/widget/FolderSelectWidget.java +++ b/src/client/java/dev/spiritstudios/snapper/gui/widget/FolderSelectWidget.java @@ -2,7 +2,6 @@ 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.ParentElement; @@ -14,19 +13,16 @@ import net.minecraft.util.Formatting; import net.minecraft.util.Identifier; import org.jetbrains.annotations.Nullable; +import org.polyfrost.oneconfig.api.config.v1.Property; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; -import static dev.spiritstudios.snapper.Snapper.LOGGER; import static dev.spiritstudios.snapper.Snapper.MODID; public class FolderSelectWidget extends ContainerWidget implements ParentElement { - private final Value value; private static final Identifier FOLDER_ICON = Identifier.of(MODID, "screenshots/folder"); private static final Identifier RESET_ICON = Identifier.of(MODID, "screenshots/reset"); @@ -35,6 +31,8 @@ public class FolderSelectWidget extends ContainerWidget implements ParentElement private final TextIconButtonWidget resetFolderButton; private final MinecraftClient client = MinecraftClient.getInstance(); + private final Property value; + /* Because of the visual bar at the top of config screens, this offset needs to exist for the mouse to notice the elements. There is probably a proper way to do this, but I cannot be bothered. @@ -43,7 +41,7 @@ public class FolderSelectWidget extends ContainerWidget implements ParentElement */ private static final int WEIRD_FIX_OFFSET = 40; - public FolderSelectWidget(int x, int y, int width, int height, Value value, String placeholderKey) { + public FolderSelectWidget(int x, int y, int width, int height, Property value, String placeholderKey) { super(x, y, width, height, ScreenTexts.EMPTY); this.value = value; this.active = false; @@ -80,7 +78,7 @@ public FolderSelectWidget(int x, int y, int width, int height, Value value this.resetFolderButton = TextIconButtonWidget.builder( Text.translatable("config.snapper.snapper.customScreenshotFolder.reset"), button -> { - value.reset(); + //value.reset(); //todo textField.setText(value.get().toString()); }, true diff --git a/src/client/java/dev/spiritstudios/snapper/gui/widget/GridListAbstraction.java b/src/client/java/dev/spiritstudios/snapper/gui/widget/GridListAbstraction.java new file mode 100644 index 0000000..88b9a7b --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/gui/widget/GridListAbstraction.java @@ -0,0 +1,8 @@ +package dev.spiritstudios.snapper.gui.widget; + +public interface GridListAbstraction { + + boolean showGrid(); + + int getColumnCount(); +} diff --git a/src/client/java/dev/spiritstudios/snapper/gui/widget/OptionsScrollableWidget.java b/src/client/java/dev/spiritstudios/snapper/gui/widget/OptionsScrollableWidget.java new file mode 100644 index 0000000..c8323e2 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/gui/widget/OptionsScrollableWidget.java @@ -0,0 +1,75 @@ +package dev.spiritstudios.snapper.gui.widget; + +import java.util.ArrayList; +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.gui.widget.ElementListWidget; + +public class OptionsScrollableWidget extends ElementListWidget { + public OptionsScrollableWidget(MinecraftClient client, int width, int height, int y, int itemHeight) { + super(client, width, height, y, itemHeight); + this.centerListVertically = false; + } + + @Override + protected int getScrollbarX() { + return super.getScrollbarX() + 32; + } + + @Override + public int getRowWidth() { + return 400; + } + + public void addOptions(List options) { + for (int i = 0; i < options.size(); i += 2) { + ClickableWidget widget = options.get(i); + ClickableWidget widget2 = i + 1 < options.size() ? options.get(i + 1) : null; + + this.addEntry(new OptionEntry(widget, widget2, this.width)); + } + } + + protected static class OptionEntry extends Entry { + private final List widgets = new ArrayList<>(); + + public OptionEntry(ClickableWidget widget, @Nullable ClickableWidget widget2, int width) { + widget.setWidth(310); + if (widget2 != null) { + widget2.setWidth(150); + widget.setWidth(150); + } + + widget.setX(width / 2 - 155); + if (widget2 != null) widget2.setX(width / 2 + 5); + + this.widgets.add(widget); + if (widget2 != null) this.widgets.add(widget2); + } + + @Override + public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + widgets.forEach(widget -> { + widget.setY(y); + widget.render(context, mouseX, mouseY, tickDelta); + }); + } + + @Override + public List selectableChildren() { + return widgets; + } + + @Override + public List children() { + return widgets; + } + } +} \ No newline at end of file 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 65f4ce5..a97a0ef 100644 --- a/src/client/java/dev/spiritstudios/snapper/gui/widget/ScreenshotListWidget.java +++ b/src/client/java/dev/spiritstudios/snapper/gui/widget/ScreenshotListWidget.java @@ -5,11 +5,7 @@ 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.ScreenshotImage; -import dev.spiritstudios.snapper.util.SnapperUtil; -import dev.spiritstudios.specter.api.core.exception.UnreachableException; +import dev.spiritstudios.snapper.util.*; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.navigation.NavigationDirection; @@ -23,7 +19,6 @@ import net.minecraft.util.Identifier; import net.minecraft.util.StringHelper; import net.minecraft.util.Util; -import net.minecraft.util.math.MathHelper; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -35,13 +30,13 @@ 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; -import java.util.stream.Collectors; -public class ScreenshotListWidget extends AlwaysSelectedEntryListWidget { +public class ScreenshotListWidget extends AlwaysSelectedEntryListWidget implements GridListAbstraction { private static final Identifier VIEW_SPRITE = Snapper.id("screenshots/view"); private static final Identifier VIEW_HIGHLIGHTED_SPRITE = Snapper.id("screenshots/view_highlighted"); @@ -51,6 +46,8 @@ public class ScreenshotListWidget extends AlwaysSelectedEntryListWidget> loadFuture; + public static final int GRID_ENTRY_WIDTH = 144; + private final int gridItemHeight = 81; private final int listItemHeight = 36; private boolean showGrid = false; @@ -71,7 +68,6 @@ public ScreenshotListWidget( this.loadFuture.thenAccept(entries -> { this.clearEntries(); - entries.sort(Comparator.comparingLong(ScreenshotEntry::lastModified).reversed()); entries.forEach(this::addEntry); if (entries.isEmpty()) { @@ -79,7 +75,7 @@ public ScreenshotListWidget( } }); - this.showGrid = SnapperConfig.INSTANCE.viewMode.get().equals(ScreenshotViewerScreen.ViewMode.GRID); + this.showGrid = SnapperConfig.viewMode.equals(ScreenshotScreen.ViewMode.GRID); ((EntryListWidgetAccessor) this).setItemHeight(this.showGrid ? this.gridItemHeight : this.listItemHeight); } @@ -102,20 +98,34 @@ public boolean keyPressed(int keyCode, int scanCode, int modifiers) { public CompletableFuture> load(MinecraftClient client) { return CompletableFuture.supplyAsync(() -> { - List screenshots = ScreenshotActions.getScreenshots(client); - return screenshots.parallelStream() - .map(file -> new ScreenshotEntry(file, client, parent, screenshots)) - .collect(Collectors.toList()); - }); + 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); + if (this.parent instanceof ScreenshotScreen screenshotScreen) { + screenshotScreen.imageSelected(entry); + } } - private int getColumnCount() { + @Override + public int getColumnCount() { if (client.currentScreen != null) { int width = client.currentScreen.width; @@ -132,7 +142,7 @@ private int getColumnCount() { @Override public int getRowWidth() { - return showGrid ? 144 * getColumnCount() + 4 * (getColumnCount() - 1) : 220; + return showGrid ? GRID_ENTRY_WIDTH * getColumnCount() + 4 * (getColumnCount() - 1) : 220; } @Override @@ -141,10 +151,9 @@ protected void renderList(DrawContext context, int mouseX, int mouseY, float del int rowLeft = this.getRowLeft(); int rowWidth = this.getRowWidth(); int entryHeight = this.itemHeight - 4; - int gridItemWidth = 144; - int entryWidth = showGrid ? gridItemWidth : rowWidth; + int entryWidth = GRID_ENTRY_WIDTH; int entryCount = this.getEntryCount(); - int spacing = showGrid ? (rowWidth - (getColumnCount() * entryWidth)) / (getColumnCount() - 1) : 0; + int spacing = (rowWidth - (getColumnCount() * entryWidth)) / (getColumnCount() - 1); for (int index = 0; index < entryCount; index++) { int rowTop = this.getRowTop(index); @@ -177,29 +186,24 @@ public int getMaxScrollY() { 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 ? ScreenshotViewerScreen.ViewMode.GRID : ScreenshotViewerScreen.ViewMode.LIST); + SnapperConfig.viewMode = (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 boolean showGrid() { + return showGrid; } public abstract static class Entry extends AlwaysSelectedEntryListWidget.Entry implements AutoCloseable { @@ -320,24 +324,21 @@ public class ScreenshotEntry extends Entry implements AutoCloseable { public final FileTime lastModified; private final MinecraftClient client; - public final ScreenshotImage icon; + public final DynamicTexture icon; public final String iconFileName; - public Path path; public final Screen screenParent; private long time; private boolean showGrid; private final List screenshots; private boolean clickthroughHovered = false; - public ScreenshotEntry(Path iconPath, MinecraftClient client, Screen parent, List screenshots) { + public ScreenshotEntry(DynamicTexture icon, MinecraftClient client, Screen parent, List screenshots) { this.showGrid = ScreenshotListWidget.this.showGrid; this.client = client; this.screenParent = parent; - this.icon = ScreenshotImage.createScreenshot(this.client.getTextureManager(), iconPath) - .orElse(null); - this.path = iconPath; - this.iconFileName = iconPath.getFileName().toString(); - this.lastModified = SafeFiles.getLastModifiedTime(iconPath).orElse(FileTime.fromMillis(0L)); + this.icon = icon; + this.iconFileName = icon.getPath().getFileName().toString(); + this.lastModified = SafeFiles.getLastModifiedTime(icon.getPath()).orElse(FileTime.fromMillis(0L)); this.screenshots = screenshots; } @@ -360,7 +361,7 @@ public void renderList(DrawContext context, int index, int y, int x, int entryWi long creationTime = 0; try { - creationTime = Files.readAttributes(path, BasicFileAttributes.class).creationTime().toMillis(); + creationTime = Files.readAttributes(icon.getPath(), BasicFileAttributes.class).creationTime().toMillis(); } catch (IOException e) { client.setScreen(new ScreenshotScreen(screenParent)); } @@ -387,7 +388,8 @@ public void renderList(DrawContext context, int index, int y, int x, int entryWi false ); - if (this.icon != null) { + if (icon.loaded()) { + //noinspection SuspiciousNameCombination context.drawTexture( RenderLayer::getGuiTextured, this.icon.getTextureId(), @@ -403,7 +405,7 @@ public void renderList(DrawContext context, int index, int y, int x, int entryWi context.fill(x, y, x + 32, y + 32, 0xA0909090); context.drawGuiTexture( RenderLayer::getGuiTextured, - mouseX - x < 32 && this.icon != null ? + mouseX - x < 32 && this.icon.loaded() ? ScreenshotListWidget.VIEW_HIGHLIGHTED_SPRITE : ScreenshotListWidget.VIEW_SPRITE, x, y, @@ -418,7 +420,7 @@ public void renderGrid(DrawContext context, int index, int y, int x, int entryWi clickthroughHovered = SnapperUtil.inBoundingBox(centreX - 16, centreY - 16, 32, 32, mouseX, mouseY); - if (this.icon != null) { + if (this.icon.loaded()) { context.drawTexture( RenderLayer::getGuiTextured, this.icon.getTextureId(), @@ -447,7 +449,7 @@ public void renderMetadata(DrawContext context, int index, int y, int x, int ent String creationString = "undefined"; long creationTime = 0; try { - creationTime = Files.readAttributes(path, BasicFileAttributes.class).creationTime().toMillis(); + creationTime = Files.readAttributes(icon.getPath(), BasicFileAttributes.class).creationTime().toMillis(); } catch (IOException e) { client.setScreen(new ScreenshotScreen(screenParent)); } @@ -467,8 +469,7 @@ public void renderMetadata(DrawContext context, int index, int y, int x, int ent context.drawGuiTexture( RenderLayer::getGuiTextured, - clickthroughHovered && - this.icon != null ? + clickthroughHovered && icon.loaded() ? ScreenshotListWidget.VIEW_HIGHLIGHTED_SPRITE : ScreenshotListWidget.VIEW_SPRITE, centreX - 16, centreY - 16, @@ -478,7 +479,7 @@ public void renderMetadata(DrawContext context, int index, int y, int x, int ent context.drawText( this.client.textRenderer, - fileName, + truncateFileName(fileName, entryWidth, 24), x + 5, y + 6, 0xFFFFFF, @@ -556,7 +557,7 @@ public boolean mouseClicked(double mouseX, double mouseY, int button) { public boolean click() { if (this.icon == null) return false; playClickSound(this.client.getSoundManager()); - this.client.setScreen(new ScreenshotViewerScreen(this.icon, this.path, this.screenParent, this.screenshots)); + this.client.setScreen(new ScreenshotViewerScreen(this.icon, icon.getPath(), this.screenParent, this.screenshots)); return true; } @@ -569,4 +570,4 @@ public long lastModified() { return lastModified.toMillis(); } } -} \ No newline at end of file +} diff --git a/src/client/java/dev/spiritstudios/snapper/gui/widget/SpecterButtonWidget.java b/src/client/java/dev/spiritstudios/snapper/gui/widget/SpecterButtonWidget.java new file mode 100644 index 0000000..a8668c9 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/gui/widget/SpecterButtonWidget.java @@ -0,0 +1,87 @@ +package dev.spiritstudios.snapper.gui.widget; + +import java.util.function.Supplier; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.text.Text; + +public class SpecterButtonWidget extends ButtonWidget { + protected final Supplier message; + + protected SpecterButtonWidget(int x, int y, int width, int height, Supplier message, PressAction onPress, NarrationSupplier narrationSupplier) { + super(x, y, width, height, message.get(), onPress, narrationSupplier); + + this.message = message; + } + + public static Builder builder(Supplier message, PressAction onPress) { + return new Builder(message, onPress); + } + + @Override + public Text getMessage() { + return message.get(); + } + + public static final class Builder { + private final Supplier message; + private final ButtonWidget.PressAction onPress; + @Nullable + private Tooltip tooltip; + private int x; + private int y; + private int width = 150; + private int height = 20; + private ButtonWidget.NarrationSupplier narrationSupplier = ButtonWidget.DEFAULT_NARRATION_SUPPLIER; + + public Builder(Supplier message, ButtonWidget.PressAction onPress) { + this.message = message; + this.onPress = onPress; + } + + public Builder position(int x, int y) { + this.x = x; + this.y = y; + return this; + } + + public Builder width(int width) { + this.width = width; + return this; + } + + public Builder height(int height) { + this.height = height; + return this; + } + + public Builder size(int width, int height) { + this.width = width; + this.height = height; + return this; + } + + public Builder dimensions(int x, int y, int width, int height) { + return this.position(x, y).size(width, height); + } + + public Builder tooltip(@Nullable Tooltip tooltip) { + this.tooltip = tooltip; + return this; + } + + public Builder narrationSupplier(ButtonWidget.NarrationSupplier narrationSupplier) { + this.narrationSupplier = narrationSupplier; + return this; + } + + public SpecterButtonWidget build() { + SpecterButtonWidget buttonWidget = new SpecterButtonWidget(this.x, this.y, this.width, this.height, this.message, this.onPress, this.narrationSupplier); + buttonWidget.setTooltip(this.tooltip); + return buttonWidget; + } + } +} \ No newline at end of file diff --git a/src/client/java/dev/spiritstudios/snapper/gui/widget/SpecterSliderWidget.java b/src/client/java/dev/spiritstudios/snapper/gui/widget/SpecterSliderWidget.java new file mode 100644 index 0000000..5f6250e --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/gui/widget/SpecterSliderWidget.java @@ -0,0 +1,263 @@ +package dev.spiritstudios.snapper.gui.widget; + +import java.util.function.Consumer; +import java.util.function.Function; + +import dev.spiritstudios.snapper.util.Range; +import org.lwjgl.glfw.GLFW; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.navigation.GuiNavigationType; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.input.KeyCodes; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.sound.SoundManager; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.ColorHelper; +import net.minecraft.util.math.MathHelper; + +public class SpecterSliderWidget extends ClickableWidget { + private static final Identifier SLIDER = Identifier.ofVanilla("widget/slider"); + private static final Identifier SLIDER_HIGHLIGHTED = Identifier.ofVanilla("widget/slider_highlighted"); + private static final Identifier SLIDER_HANDLE = Identifier.ofVanilla("widget/slider_handle"); + private static final Identifier SLIDER_HANDLE_HIGHLIGHTED = Identifier.ofVanilla("widget/slider_handle_highlighted"); + + private static final Range ZERO_ONE = new Range<>(0.0, 1.0); + protected final double step; + protected final Range range; + protected final Consumer valueChangedListener; + protected final Function messageSupplier; + protected double value; + protected boolean sliderFocused; + + protected SpecterSliderWidget(int x, int y, int width, int height, double value, double step, Range range, Consumer valueChangedListener, Function messageSupplier) { + super(x, y, width, height, messageSupplier.apply(value)); + + this.value = value; + this.step = step; + this.range = range; + this.valueChangedListener = valueChangedListener; + this.messageSupplier = messageSupplier; + } + + public static Builder builder(double value) { + return new Builder(value); + } + + // region Input + @Override + public void onClick(double mouseX, double mouseY) { + this.setValueFromMouse(mouseX); + } + + @Override + protected void onDrag(double mouseX, double mouseY, double deltaX, double deltaY) { + this.setValueFromMouse(mouseX); + super.onDrag(mouseX, mouseY, deltaX, deltaY); + } + + @Override + public void onRelease(double mouseX, double mouseY) { + super.playDownSound(MinecraftClient.getInstance().getSoundManager()); + } + + private void setValueFromMouse(double mouseX) { + setValue(range.map(MathHelper.clamp((mouseX - (double) (this.getX() + 4)) / (double) (this.getWidth() - 8), 0.0, 1.0), ZERO_ONE)); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (KeyCodes.isToggle(keyCode)) { + this.sliderFocused = !this.sliderFocused; + return true; + } + + if (!this.sliderFocused) return false; + + if (keyCode == GLFW.GLFW_KEY_LEFT || keyCode == GLFW.GLFW_KEY_RIGHT) { + float sign = keyCode == GLFW.GLFW_KEY_LEFT ? -1.0F : 1.0F; + this.setValue(this.value + sign * (this.step == 0.0 ? 0.01 : this.step)); + + return true; + } + + return false; + } + // endregion + + // region Navigation + @Override + public void setFocused(boolean focused) { + super.setFocused(focused); + if (!focused) { + this.sliderFocused = false; + return; + } + + GuiNavigationType navigationType = MinecraftClient.getInstance().getNavigationType(); + if (navigationType == GuiNavigationType.MOUSE || navigationType == GuiNavigationType.KEYBOARD_TAB) + this.sliderFocused = true; + } + + protected void onValueChanged() { + this.valueChangedListener.accept(value); + } + + @Override + public void playDownSound(SoundManager soundManager) { + } + // endregion + + // region Rendering + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + MinecraftClient client = MinecraftClient.getInstance(); + + context.drawGuiTexture( + RenderLayer::getGuiTextured, + this.getTexture(), + this.getX(), + this.getY(), + this.getWidth(), + this.getHeight(), + ColorHelper.fromFloats(this.alpha, 1.0F, 1.0F, 1.0F) + ); + + context.drawGuiTexture( + RenderLayer::getGuiTextured, + this.getHandleTexture(), + this.getX() + (int) (ZERO_ONE.map(this.value, range) * (this.getWidth() - 8)), + this.getY(), + 8, + this.getHeight(), + ColorHelper.fromFloats(this.alpha, 1.0F, 1.0F, 1.0F) + ); + + int color = this.active ? 0xffffff : 0xa0a0a0; + + this.drawScrollableText(context, client.textRenderer, 2, color | MathHelper.ceil(this.alpha * 255.0F) << 24); + } + + @Override + public Text getMessage() { + return this.messageSupplier.apply(value); + } + + protected Identifier getTexture() { + return this.isFocused() && !this.sliderFocused ? SLIDER_HIGHLIGHTED : SLIDER; + } + + protected Identifier getHandleTexture() { + return !this.hovered && !this.sliderFocused ? SLIDER_HANDLE : SLIDER_HANDLE_HIGHLIGHTED; + } + // endregion + + // region Narration + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + builder.put(NarrationPart.TITLE, this.getNarrationMessage()); + if (!this.active) return; + + builder.put( + NarrationPart.USAGE, + Text.translatable(isFocused() ? "narration.slider.usage.focused" : "narration.slider.usage.hovered") + ); + } + + @Override + protected MutableText getNarrationMessage() { + return Text.translatable("gui.narrate.slider", this.getMessage()); + } + // endregion + + protected void setValue(double value) { + double oldValue = this.value; + + double newValue = value; + newValue = step <= 0.0 ? newValue : range.map(Math.round(ZERO_ONE.map(newValue, range) / step) * step, ZERO_ONE); + this.value = range.clamp(newValue); + + if (oldValue != this.value) onValueChanged(); + } + + public static class Builder { + private final double value; + private int x; + private int y; + private int width = 150; + private int height = 20; + private double step; + private Range range = new Range<>(0.0, 1.0); + private Consumer valueChangedListener = value -> { + }; + private Function messageSupplier = (value) -> Text.of(String.format("%.2f", value)); + + protected Builder(double value) { + this.value = value; + } + + public Builder position(int x, int y) { + this.x = x; + this.y = y; + return this; + } + + public Builder size(int width, int height) { + this.width = width; + this.height = height; + return this; + } + + public Builder dimensions(int width, int height, int x, int y) { + return position(x, y).size(width, height); + } + + public Builder message(Text message) { + messageSupplier = (ignored) -> message; + return this; + } + + public Builder message(Function messageSupplier) { + this.messageSupplier = messageSupplier; + return this; + } + + public Builder step(double step) { + this.step = step; + return this; + } + + public Builder range(Range range) { + this.range = range; + return this; + } + + public Builder range(double min, double max) { + return range(new Range<>(min, max)); + } + + public Builder onValueChanged(Consumer valueChangedListener) { + this.valueChangedListener = valueChangedListener; + return this; + } + + public SpecterSliderWidget build() { + return new SpecterSliderWidget( + x, + y, + width, + height, + value, + step, + range, + valueChangedListener, + messageSupplier + ); + } + } +} \ No newline at end of file diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/EntryListWidgetMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/EntryListWidgetMixin.java new file mode 100644 index 0000000..f959449 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/mixin/EntryListWidgetMixin.java @@ -0,0 +1,50 @@ +package dev.spiritstudios.snapper.mixin; + +import dev.spiritstudios.snapper.gui.widget.GridListAbstraction; +import net.minecraft.client.gui.widget.EntryListWidget; +import net.minecraft.client.gui.widget.ScrollableWidget; +import net.minecraft.util.math.MathHelper; +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.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.List; + +@Mixin(EntryListWidget.class) +public abstract class EntryListWidgetMixin { + + @Shadow public abstract int getRowWidth(); + + @Shadow public abstract int getRowLeft(); + + @Shadow protected int headerHeight; + + @Shadow @Final protected int itemHeight; + + @Shadow protected abstract int getEntryCount(); + + @Shadow @Final private List children; + + @Inject(method = "getEntryAtPosition", at = @At("HEAD"), cancellable = true) + private void snapper$getEntryAtPosition(double x, double y, CallbackInfoReturnable cir) { + if (this instanceof GridListAbstraction gridList) { + if (gridList.showGrid()) { + ScrollableWidget scrollableWidget = (ScrollableWidget) (Object) this; + int rowWidth = this.getRowWidth(); + int relX = MathHelper.floor(x - this.getRowLeft()); + int relY = MathHelper.floor(y - (double) scrollableWidget.getY()) - this.headerHeight; + + if (relX < 0 || relX > rowWidth || relY < 0 || relY > scrollableWidget.getBottom()) cir.setReturnValue(null); + + int rowIndex = (relY + (int) scrollableWidget.getScrollY()) / this.itemHeight; + int colIndex = MathHelper.floor(((float) relX / (float) rowWidth) * (float) gridList.getColumnCount()); + int entryIndex = rowIndex * gridList.getColumnCount() + colIndex; + + cir.setReturnValue(entryIndex >= 0 && entryIndex < getEntryCount() ? this.children.get(entryIndex) : null); + } + } + } +} diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/GameMenuMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/GameMenuMixin.java index 4ab5a70..b826468 100644 --- a/src/client/java/dev/spiritstudios/snapper/mixin/GameMenuMixin.java +++ b/src/client/java/dev/spiritstudios/snapper/mixin/GameMenuMixin.java @@ -1,5 +1,6 @@ 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; @@ -13,8 +14,6 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import static dev.spiritstudios.snapper.Snapper.MODID; - @Mixin(GameMenuScreen.class) public abstract class GameMenuMixin extends Screen { protected GameMenuMixin(Text title) { @@ -22,14 +21,14 @@ protected GameMenuMixin(Text title) { } @Unique - private static final Identifier SNAPPER_BUTTON_ICON = Identifier.of(MODID, "screenshots/screenshot"); + 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()) { + if (SnapperConfig.showSnapperGameMenu) { this.addDrawableChild( TextIconButtonWidget.builder( Text.translatable("button.snapper.screenshots"), diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/KeyboardMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/KeyboardMixin.java index a57420f..a2fa60a 100644 --- a/src/client/java/dev/spiritstudios/snapper/mixin/KeyboardMixin.java +++ b/src/client/java/dev/spiritstudios/snapper/mixin/KeyboardMixin.java @@ -23,11 +23,11 @@ public abstract class KeyboardMixin { @Overwrite private void method_1464(Text text) { this.client.inGameHud.setOverlayMessage(Text.translatable( - SnapperConfig.INSTANCE.copyTakenScreenshot.get() ? + SnapperConfig.copyTakenScreenshot ? "text.snapper.screenshot_instructions_copy" : "text.snapper.screenshot_instructions", text, - SnapperKeybindings.RECENT_SCREENSHOT_KEY.getBoundKeyLocalizedText() + SnapperKeybindings.RECENT_SCREENSHOT_KEY.getVanillaKeyBinding().getBoundKeyLocalizedText() ), false); } } diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/ScreenshotRecorderMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/ScreenshotRecorderMixin.java index bea7c0d..b0eac84 100644 --- a/src/client/java/dev/spiritstudios/snapper/mixin/ScreenshotRecorderMixin.java +++ b/src/client/java/dev/spiritstudios/snapper/mixin/ScreenshotRecorderMixin.java @@ -1,20 +1,22 @@ package dev.spiritstudios.snapper.mixin; +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; 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 org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; import java.util.function.Consumer; @Mixin(ScreenshotRecorder.class) @@ -25,7 +27,7 @@ public abstract class ScreenshotRecorderMixin { */ @SuppressWarnings("ResultOfMethodCallIgnored") @Inject( - method = "method_1661", + method = "method_22691", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/texture/NativeImage;writeTo(Ljava/io/File;)V") ) private static void lookBeforeYouLeap(NativeImage nativeImage, File screenshotFile, Consumer messageReceiver, CallbackInfo ci) throws IOException { @@ -34,22 +36,24 @@ private static void lookBeforeYouLeap(NativeImage nativeImage, File screenshotFi } @Inject( - method = "method_1661", + method = "method_22691", at = @At(value = "INVOKE", target = "Lnet/minecraft/text/Text;literal(Ljava/lang/String;)Lnet/minecraft/text/MutableText;", shift = At.Shift.AFTER) ) - private static void saveWrittenFileToClipboard(NativeImage nativeImage, File screenshotFile, Consumer messageReceiver, CallbackInfo ci) throws IOException { - if (!screenshotFile.getAbsolutePath().contains("/panorama/") && SnapperConfig.INSTANCE.copyTakenScreenshot.get()) { + private static void saveWrittenFileToClipboard(NativeImage nativeImage, File screenshotFile, Consumer messageReceiver, CallbackInfo ci) { + if (!screenshotFile.getAbsolutePath().contains("/panorama/") && SnapperConfig.copyTakenScreenshot) { Snapper.getPlatformHelper().copyScreenshot(screenshotFile.toPath()); } } - @ModifyArg(method = "saveScreenshot(Ljava/io/File;Ljava/lang/String;Lnet/minecraft/client/gl/Framebuffer;Ljava/util/function/Consumer;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/ScreenshotRecorder;saveScreenshotInner(Ljava/io/File;Ljava/lang/String;Lnet/minecraft/client/gl/Framebuffer;Ljava/util/function/Consumer;)V", ordinal = 0)) - private static File getConfiguredGameDirectory(File gameDirectory) { - Path customScreenshotFolder = SnapperConfig.INSTANCE.customScreenshotFolder.get(); - - if (SnapperConfig.INSTANCE.useCustomScreenshotFolder.get() && Files.exists(customScreenshotFolder)) { - return customScreenshotFolder.toFile(); - } - return gameDirectory; + @WrapMethod(method = "saveScreenshot(Ljava/io/File;Ljava/lang/String;Lnet/minecraft/client/gl/Framebuffer;Ljava/util/function/Consumer;)V") + private static void getConfiguredGameDirectory(File gameDirectory, @Nullable String fileName, Framebuffer framebuffer, Consumer messageReceiver, Operation original) { + original.call( + SnapperConfig.useCustomScreenshotFolder && Files.exists(SnapperConfig.customScreenshotFolder) ? + SnapperConfig.customScreenshotFolder.toFile() : + gameDirectory, + fileName, + framebuffer, + messageReceiver + ); } } diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/TitleScreenMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/TitleScreenMixin.java index 9f227ab..3b26883 100644 --- a/src/client/java/dev/spiritstudios/snapper/mixin/TitleScreenMixin.java +++ b/src/client/java/dev/spiritstudios/snapper/mixin/TitleScreenMixin.java @@ -1,5 +1,6 @@ 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.Screen; @@ -13,12 +14,12 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import static dev.spiritstudios.snapper.Snapper.MODID; +import java.util.Objects; @Mixin(TitleScreen.class) public abstract class TitleScreenMixin extends Screen { @Unique - private static final Identifier SNAPPER_BUTTON_ICON = Identifier.of(MODID, "screenshots/screenshot"); + private static final Identifier SNAPPER_BUTTON_ICON = Snapper.id("screenshots/screenshot"); protected TitleScreenMixin(Text title) { super(title); @@ -29,11 +30,12 @@ protected TitleScreenMixin(Text title) { at = @At("HEAD") ) protected void init(CallbackInfo ci) { - if (this.client == null) return; - int y = this.height / 4 + 48; - int spacingY = 24; + if (SnapperConfig.showSnapperTitleScreen) { + Objects.requireNonNull(client); + + int y = this.height / 4 + 48; + int spacingY = 24; - if (SnapperConfig.INSTANCE.showSnapperTitleScreen.get()) { this.addDrawableChild( TextIconButtonWidget.builder( Text.translatable("button.snapper.screenshots"), diff --git a/src/client/java/dev/spiritstudios/snapper/util/ScreenshotImage.java b/src/client/java/dev/spiritstudios/snapper/util/DynamicTexture.java similarity index 53% rename from src/client/java/dev/spiritstudios/snapper/util/ScreenshotImage.java rename to src/client/java/dev/spiritstudios/snapper/util/DynamicTexture.java index ab825a3..b47386c 100644 --- a/src/client/java/dev/spiritstudios/snapper/util/ScreenshotImage.java +++ b/src/client/java/dev/spiritstudios/snapper/util/DynamicTexture.java @@ -1,5 +1,7 @@ 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; @@ -11,37 +13,43 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; +import java.util.concurrent.CompletableFuture; -public class ScreenshotImage implements AutoCloseable { - private static final Identifier UNKNOWN_SERVER_ID = Identifier.ofVanilla("textures/misc/unknown_server.png"); +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 NativeImageBackedTexture texture; + private final Path path; - private boolean closed; + private final NativeImage image; + private NativeImageBackedTexture texture; - private ScreenshotImage(TextureManager textureManager, Identifier id, Path path) throws IOException { + private DynamicTexture(TextureManager textureManager, Identifier id, Path path) throws IOException { this.textureManager = textureManager; this.id = id; - if (!Files.isRegularFile(path)) - throw new IllegalArgumentException("Passed path for invalid file to new ScreenshotImage()"); + this.path = path; try (InputStream stream = Files.newInputStream(path)) { - this.texture = new NativeImageBackedTexture(NativeImage.read(stream)); + this.image = NativeImage.read(stream); } + } - this.textureManager.registerTexture(this.id, this.texture); + 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) { + public static Optional createScreenshot(TextureManager textureManager, Path path) { try { - return Optional.of(new ScreenshotImage( + return Optional.of(new DynamicTexture( textureManager, - Identifier.ofVanilla( - "screenshots/" + Util.replaceInvalidChars(path.getFileName().toString(), Identifier::isPathCharacterValid) + "/icon" - ), + Snapper.id( + "screenshots/" + Util.replaceInvalidChars(path.getFileName().toString(), Identifier::isPathCharacterValid) + "/icon" + ), path )); } catch (IOException e) { @@ -49,11 +57,11 @@ public static Optional createScreenshot(TextureManager textureM } } - public static Optional createPanoramaFace(TextureManager textureManager, Path path) { + public static Optional createPanoramaFace(TextureManager textureManager, Path path) { try { - return Optional.of(new ScreenshotImage( + return Optional.of(new DynamicTexture( textureManager, - Identifier.ofVanilla( + Snapper.id( "screenshots/panorama/" + Util.replaceInvalidChars(path.getFileName().toString(), Identifier::isPathCharacterValid) ), path @@ -67,37 +75,35 @@ public static Optional createPanoramaFace(TextureManager textur * Must be called on render thread */ public void enableFiltering() { - this.assertOpen(); - this.texture.setFilter(true, false); + this.texture.setFilter(true, true); } public void destroy() { - this.assertOpen(); - this.textureManager.destroyTexture(this.id); this.texture.close(); } public int getWidth() { - this.assertOpen(); return this.texture != null && this.texture.getImage() != null ? this.texture.getImage().getWidth() : 64; } public int getHeight() { - this.assertOpen(); return this.texture != null && this.texture.getImage() != null ? this.texture.getImage().getHeight() : 64; } public Identifier getTextureId() { - return this.texture != null ? this.id : UNKNOWN_SERVER_ID; + return this.texture != null ? this.id : UNKNOWN_SERVER; } - public void close() { - this.destroy(); - this.closed = true; + public boolean loaded() { + return texture != null; } - private void assertOpen() { - if (this.closed) throw new IllegalStateException("Icon already closed"); + public Path getPath() { + return path; + } + + public void close() { + this.destroy(); } } diff --git a/src/client/java/dev/spiritstudios/snapper/util/GenericMath.java b/src/client/java/dev/spiritstudios/snapper/util/GenericMath.java new file mode 100644 index 0000000..a71b4d9 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/util/GenericMath.java @@ -0,0 +1,117 @@ +package dev.spiritstudios.snapper.util; + +import java.util.function.BiFunction; + +/** + * A few very cursed methods for doing arithmetic with {@link Number}s. + */ +public final class GenericMath { + private static final Ops BYTE_OPS = Ops.create( + (a, b) -> (byte) (a + b), + (a, b) -> (byte) (a - b), + (a, b) -> (byte) (a * b), + (a, b) -> (byte) (a / b) + ); + private static final Ops SHORT_OPS = Ops.create( + (a, b) -> (short) (a + b), + (a, b) -> (short) (a - b), + (a, b) -> (short) (a * b), + (a, b) -> (short) (a / b) + ); + private static final Ops INTEGER_OPS = Ops.create( + Integer::sum, + (a, b) -> a - b, + (a, b) -> a * b, + (a, b) -> a / b + ); + private static final Ops LONG_OPS = Ops.create( + Long::sum, + (a, b) -> a - b, + (a, b) -> a * b, + (a, b) -> a / b + ); + private static final Ops FLOAT_OPS = Ops.create( + Float::sum, + (a, b) -> a - b, + (a, b) -> a * b, + (a, b) -> a / b + ); + private static final Ops DOUBLE_OPS = Ops.create( + Double::sum, + (a, b) -> a - b, + (a, b) -> a * b, + (a, b) -> a / b + ); + + private GenericMath() { + throw new UnsupportedOperationException("Cannot instantiate utility class."); + } + + @SuppressWarnings("unchecked") + private static Ops getOps(T x) { + return switch (x) { + case Byte ignored -> (Ops) BYTE_OPS; + case Short ignored -> (Ops) SHORT_OPS; + case Integer ignored -> (Ops) INTEGER_OPS; + case Long ignored -> (Ops) LONG_OPS; + case Float ignored -> (Ops) FLOAT_OPS; + case Double ignored -> (Ops) DOUBLE_OPS; + default -> throw new IllegalArgumentException("Unsupported number type: " + x.getClass()); + }; + } + + public static T add(T a, T b) { + return getOps(a).add(a, b); + } + + public static T subtract(T a, T b) { + return getOps(a).subtract(a, b); + } + + public static T multiply(T a, T b) { + return getOps(a).multiply(a, b); + } + + public static T divide(T a, T b) { + return getOps(a).divide(a, b); + } + + /** + * A set of arithmetic operations that can be performed on {@link Number}s. + * + * @param The type of number to perform operations on. + */ + public abstract static class Ops { + public static Ops create(BiFunction add, BiFunction subtract, BiFunction multiply, BiFunction divide) { + return new Ops<>() { + @Override + T add(T a, T b) { + return add.apply(a, b); + } + + @Override + T subtract(T a, T b) { + return subtract.apply(a, b); + } + + @Override + T multiply(T a, T b) { + return multiply.apply(a, b); + } + + @Override + T divide(T a, T b) { + return divide.apply(a, b); + } + }; + } + + abstract T add(T a, T b); + + abstract T subtract(T a, T b); + + abstract T multiply(T a, T b); + + abstract T divide(T a, T b); + } +} \ No newline at end of file diff --git a/src/client/java/dev/spiritstudios/snapper/util/PatternMap.java b/src/client/java/dev/spiritstudios/snapper/util/PatternMap.java new file mode 100644 index 0000000..c3c3018 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/util/PatternMap.java @@ -0,0 +1,92 @@ +package dev.spiritstudios.snapper.util; + +import java.util.*; +import java.util.stream.Collectors; + +import com.mojang.datafixers.util.Pair; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A map that allows for pattern matching (instanceof) when retrieving values. + * This is extremely inefficient due to us having to iterate over all entries to find a match. + * Unfortunately, this is the best we can do if we want to check for inheritance. + * + * @param The value type + */ +public class PatternMap implements Map, V> { + private final List, V>> entries = new ArrayList<>(); + + public V get(Class clazz) { + return entries.stream(). + filter(entry -> entry.getFirst().isAssignableFrom(clazz)) + .findFirst() + .map(Pair::getSecond) + .orElse(null); + } + + @Override + public int size() { + return entries.size(); + } + + @Override + public boolean isEmpty() { + return entries.isEmpty(); + } + + @Override + public boolean containsKey(Object o) { + return entries.stream().anyMatch(entry -> entry.getFirst().isAssignableFrom((Class) o)); + } + + @Override + public boolean containsValue(Object o) { + return entries.stream().anyMatch(entry -> entry.getSecond().equals(o)); + } + + public V get(Object object) { + return get(object.getClass()); + } + + @Override + public @Nullable V put(Class aClass, V v) { + V old = get(aClass); + entries.add(Pair.of(aClass, v)); + return old; + } + + @Override + public V remove(Object o) { + V value = get(o); + entries.removeIf(pair -> pair.getFirst().equals(o)); + return value; + } + + @Override + public void putAll(@NotNull Map, ? extends V> map) { + map.forEach(this::put); + } + + @Override + public void clear() { + entries.clear(); + } + + @Override + public @NotNull Set> keySet() { + return entries.stream().map(Pair::getFirst).collect(Collectors.toSet()); + } + + @Override + public @NotNull Collection values() { + return entries.stream().map(Pair::getSecond).collect(Collectors.toList()); + } + + @Override + public @NotNull Set, V>> entrySet() { + return entries.stream() + .map(pair -> new AbstractMap.SimpleEntry, V>(pair.getFirst(), pair.getSecond())) + .collect(Collectors.toSet()); + } +} \ No newline at end of file diff --git a/src/client/java/dev/spiritstudios/snapper/util/Range.java b/src/client/java/dev/spiritstudios/snapper/util/Range.java new file mode 100644 index 0000000..b036d25 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/util/Range.java @@ -0,0 +1,31 @@ +package dev.spiritstudios.snapper.util; + +public record Range>(T min, T max) { + public boolean contains(T value) { + return value.compareTo(min) >= 0 && value.compareTo(max) <= 0; + } + + public T clamp(T value) { + return value.compareTo(min) < 0 ? min : value.compareTo(max) > 0 ? max : value; + } + + public T range() { + return GenericMath.subtract(max, min); + } + + public T lerp(T delta) { + return GenericMath.add(min, GenericMath.multiply(delta, range())); + } + + public T lerpProgress(T value) { + return GenericMath.divide(GenericMath.subtract(value, min), range()); + } + + public T map(T value, Range from) { + return lerp(from.lerpProgress(value)); + } + + public T map01(T value) { + return GenericMath.add(GenericMath.multiply(value, range()), min); + } +} \ No newline at end of file diff --git a/src/client/java/dev/spiritstudios/snapper/util/ScreenshotActions.java b/src/client/java/dev/spiritstudios/snapper/util/ScreenshotActions.java index 23f7f70..ca8f405 100644 --- a/src/client/java/dev/spiritstudios/snapper/util/ScreenshotActions.java +++ b/src/client/java/dev/spiritstudios/snapper/util/ScreenshotActions.java @@ -1,110 +1,69 @@ package dev.spiritstudios.snapper.util; import dev.spiritstudios.snapper.Snapper; -import dev.spiritstudios.snapper.SnapperConfig; 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 org.jetbrains.annotations.NotNull; -import java.awt.*; -import java.awt.datatransfer.DataFlavor; -import java.awt.datatransfer.Transferable; -import java.awt.datatransfer.UnsupportedFlavorException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.FileTime; import java.util.Comparator; import java.util.List; -import java.util.Objects; import java.util.stream.Stream; public class ScreenshotActions { - public static void deleteScreenshot(Path path, Screen screen) { - if (!Files.exists(path)) return; + public static void deleteScreenshot(Path path, Screen screen) { + if (!Files.exists(path)) return; - MinecraftClient client = MinecraftClient.getInstance(); - client.setScreen( - new ConfirmScreen( - confirmed -> { - if (confirmed) { - client.setScreen(new ProgressScreen(true)); - try { - Files.deleteIfExists(path); - } catch (IOException e) { - Snapper.LOGGER.error("Failed to delete file", e); - } - } - client.setScreen(screen); - }, - Text.translatable("text.snapper.delete_question"), - Text.translatable("text.snapper.delete_warning", path.getFileName()), - Text.translatable("button.snapper.delete"), - ScreenTexts.CANCEL - ) - ); - } + MinecraftClient client = MinecraftClient.getInstance(); + client.setScreen( + new ConfirmScreen( + confirmed -> { + if (confirmed) { + client.setScreen(new ProgressScreen(true)); + try { + Files.deleteIfExists(path); + } catch (IOException e) { + Snapper.LOGGER.error("Failed to delete file", e); + } + } + client.setScreen(screen); + }, + Text.translatable("text.snapper.delete_question"), + Text.translatable("text.snapper.delete_warning", path.getFileName()), + Text.translatable("button.snapper.delete"), + ScreenTexts.CANCEL + ) + ); + } - public static void renameScreenshot(Path screenshot, String newName) { - if (Files.exists(screenshot)) { - try { - Files.move(screenshot, screenshot.getParent().resolve(newName)); - } catch (IOException e) { - Snapper.LOGGER.error("Failed to rename file", e); - } - } - } + public static void renameScreenshot(Path screenshot, String newName) { + if (Files.exists(screenshot)) { + try { + Files.move(screenshot, screenshot.getParent().resolve(newName)); + } catch (IOException e) { + Snapper.LOGGER.error("Failed to rename file", e); + } + } + } - public static List getScreenshots(MinecraftClient client) { - Path customScreenshotDirectory = SnapperConfig.INSTANCE.customScreenshotFolder.get().resolve("screenshots"); - Path defaultScreenshotDirectory = Path.of(client.runDirectory.getPath(), "screenshots"); - Path screenshotDir = SnapperConfig.INSTANCE.useCustomScreenshotFolder.get() ? customScreenshotDirectory : defaultScreenshotDirectory; - - try (Stream stream = Files.list(screenshotDir)){ - return stream.filter(file -> { - if (Files.isDirectory(file)) return false; - String fileType; - - try { - fileType = Files.probeContentType(file); - } catch (IOException e) { - Snapper.LOGGER.error("Couldn't load screenshot list", e); - return false; - } - - return Objects.equals(fileType, "image/png"); - }) - .sorted(Comparator.comparingLong(path -> SafeFiles.getLastModifiedTime(path) - .map(FileTime::toMillis).orElse(0L)).reversed()) - .toList(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - record TransferableImage(Image image) implements Transferable { - @Override - public DataFlavor[] getTransferDataFlavors() { - return new DataFlavor[]{ - DataFlavor.imageFlavor - }; - } - - @Override - public boolean isDataFlavorSupported(DataFlavor flavor) { - return DataFlavor.imageFlavor.equals(flavor); - } - - @NotNull - @Override - public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { - if (!isDataFlavorSupported(flavor)) throw new UnsupportedFlavorException(flavor); - - return image(); - } - } + public static List getScreenshots() { + try (Stream stream = Files.list(SnapperUtil.getConfiguredScreenshotDirectory())) { + return stream.filter(path -> + !Files.isDirectory(path) && SafeFiles.isContentType(path, "image/png", ".png")) + .sorted(Comparator.comparingLong(path -> + SafeFiles.getLastModifiedTime(path) + .map(FileTime::toMillis) + .orElse(0L)) + .reversed()) + .toList(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/client/java/dev/spiritstudios/snapper/util/SnapperUtil.java b/src/client/java/dev/spiritstudios/snapper/util/SnapperUtil.java index 5f01f4c..54e18d9 100644 --- a/src/client/java/dev/spiritstudios/snapper/util/SnapperUtil.java +++ b/src/client/java/dev/spiritstudios/snapper/util/SnapperUtil.java @@ -6,49 +6,46 @@ import net.minecraft.util.Util; import org.apache.commons.lang3.SystemProperties; -import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -public class SnapperUtil { - 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 Path getOSUnifiedFolder() { - return switch (Util.getOperatingSystem()) { - case WINDOWS -> Path.of(System.getenv("APPDATA"), ".snapper"); - case OSX -> Path.of(SystemProperties.getUserHome() + "/Library/Application Support", "snapper"); - default -> Path.of(SystemProperties.getUserHome(), ".snapper"); - }; - } - - public static Path getConfiguredScreenshotDirectory() { - if (SnapperConfig.INSTANCE.useCustomScreenshotFolder.get()) { - Path customPath = SnapperConfig.INSTANCE.customScreenshotFolder.get().resolve("screenshots"); - try { - Files.createDirectories(customPath); - } catch (IOException e) { - Snapper.LOGGER.error("Failed to create directories of configured custom screenshot folder"); - } - return customPath; - } - return MinecraftClient.getInstance().runDirectory.toPath().resolve("screenshots"); - } - - public static boolean isOfflineAccount() { - return MinecraftClient.getInstance().getSession().getAccessToken().length() < 400; - } - - public static boolean panoramaPresent(Path path) { - if (!Files.exists(path)) return false; - int partsPresent = 0; - - for (int i = 0; i < 6; i++) { - if (Files.exists(path.resolve("panorama_%s.png".formatted(i)))) partsPresent++; - } - - return partsPresent == 6; - } +public final class SnapperUtil { + public static final Path UNIFIED_FOLDER = switch (Util.getOperatingSystem()) { + case WINDOWS -> Path.of(System.getenv("APPDATA"), ".snapper"); + case OSX -> Path.of(SystemProperties.getUserHome(), "Library", "Application Support", "snapper"); + default -> Path.of(SystemProperties.getUserHome(), ".snapper"); + }; + + 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 Path getConfiguredScreenshotDirectory() { + if (SnapperConfig.useCustomScreenshotFolder) { + Path customPath = SnapperConfig.customScreenshotFolder.resolve("screenshots"); + + if (!SafeFiles.createDirectories(customPath)) { + Snapper.LOGGER.error("Failed to create directories of configured custom screenshot folder"); + } + + return customPath; + } + + return MinecraftClient.getInstance().runDirectory.toPath().resolve("screenshots"); + } + + public static boolean isOfflineAccount() { + return MinecraftClient.getInstance().getSession().getAccessToken().length() < 400; + } + + public static boolean panoramaPresent(Path path) { + if (!Files.exists(path) || !Files.isDirectory(path)) return false; + + for (int i = 0; i < 6; i++) { + if (!Files.exists(path.resolve("panorama_%s.png".formatted(i)))) return false; + } + + return true; + } } diff --git a/src/client/java/dev/spiritstudios/snapper/util/UnreachableException.java b/src/client/java/dev/spiritstudios/snapper/util/UnreachableException.java new file mode 100644 index 0000000..2766376 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/util/UnreachableException.java @@ -0,0 +1,17 @@ +package dev.spiritstudios.snapper.util; + +/** + * An exception indicating that a piece of code should be unreachable. + * Commonly used in switch statements to indicate that all possible cases have been handled. + *

+ * If this exception is thrown, it should be considered a bug in the code. + */ +public class UnreachableException extends RuntimeException { + public UnreachableException() { + super("This error should be impossible. If you see this, please report it!"); + } + + public UnreachableException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/client/java/dev/spiritstudios/snapper/util/WindowsActions.java b/src/client/java/dev/spiritstudios/snapper/util/WindowsActions.java index b804f83..97a5cba 100644 --- a/src/client/java/dev/spiritstudios/snapper/util/WindowsActions.java +++ b/src/client/java/dev/spiritstudios/snapper/util/WindowsActions.java @@ -1,10 +1,14 @@ package dev.spiritstudios.snapper.util; import dev.spiritstudios.snapper.Snapper; +import org.jetbrains.annotations.NotNull; import javax.imageio.ImageIO; import java.awt.*; import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; @@ -15,13 +19,16 @@ public class WindowsActions implements PlatformHelper { @Override public void copyScreenshot(Path path) { - if (!Files.exists(path)) return; + if (!Files.exists(path)) { + Snapper.LOGGER.warn("Attempted to copy screenshot {} that does not exist", path); + return; + } try (InputStream stream = Files.newInputStream(path)) { BufferedImage imageBuffer = ImageIO.read(stream); getClipboard().ifPresent(clipboard -> - clipboard.setContents(new ScreenshotActions.TransferableImage(imageBuffer), null)); + clipboard.setContents(new TransferableImage(imageBuffer), null)); } catch (IOException e) { Snapper.LOGGER.error("Copying of image at {} failed", path); } @@ -32,8 +39,29 @@ private static Optional getClipboard() { return Optional.of(Toolkit.getDefaultToolkit().getSystemClipboard()); } catch (HeadlessException e) { Snapper.LOGGER.error("Failed to get clipboard", e); - } - - return Optional.empty(); + return Optional.empty(); + } } + + record TransferableImage(Image image) implements Transferable { + @Override + public DataFlavor[] getTransferDataFlavors() { + return new DataFlavor[] { + DataFlavor.imageFlavor + }; + } + + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) { + return DataFlavor.imageFlavor.equals(flavor); + } + + @NotNull + @Override + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { + if (!isDataFlavorSupported(flavor)) throw new UnsupportedFlavorException(flavor); + + return image(); + } + } } diff --git a/src/client/java/dev/spiritstudios/snapper/util/config/DirectoryConfigUtil.java b/src/client/java/dev/spiritstudios/snapper/util/config/DirectoryConfigUtil.java index 4d89c25..45678aa 100644 --- a/src/client/java/dev/spiritstudios/snapper/util/config/DirectoryConfigUtil.java +++ b/src/client/java/dev/spiritstudios/snapper/util/config/DirectoryConfigUtil.java @@ -1,40 +1,14 @@ package dev.spiritstudios.snapper.util.config; -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; -import java.io.IOException; -import java.nio.file.Files; 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( - string -> { - Path path = Path.of(string); - - try { - Files.createDirectories(path); - } catch (IOException e) { - return DataResult.error(e::getMessage); - } - - if (!Files.exists(path)) { - return DataResult.error(() -> "Failed to get file from config string value. Does the directory exist?"); - } - - return DataResult.success(path); - }, - path -> escapePath(path.toString()) - ); public static CompletableFuture> openFolderSelect(String title) { return CompletableFuture.supplyAsync(() -> TinyFileDialogs.tinyfd_selectFolderDialog(title, SystemProperties.getUserHome())) @@ -46,14 +20,4 @@ public static CompletableFuture> openFolderSelect(String title) { return Optional.of(Path.of(selectedPath)); }); } - - 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("\\", "\\\\"); - } } \ No newline at end of file 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 4dde727..63c09c5 100644 --- a/src/client/java/dev/spiritstudios/snapper/util/uploading/AxolotlClientApi.java +++ b/src/client/java/dev/spiritstudios/snapper/util/uploading/AxolotlClientApi.java @@ -11,6 +11,7 @@ import net.minecraft.util.Util; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.RandomStringUtils; +import org.jetbrains.annotations.Nullable; import java.io.Closeable; import java.io.IOException; @@ -42,7 +43,7 @@ public enum TermsAcceptance { .build(); private Instant authTime = Instant.EPOCH; - private AxolotlAuthentication auth; + private @Nullable AxolotlAuthentication auth; public CompletableFuture uploadImage(Path image) { byte[] bytes; @@ -99,7 +100,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.termsAccepted != 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 73528be..e12be25 100644 --- a/src/client/java/dev/spiritstudios/snapper/util/uploading/ScreenshotUploading.java +++ b/src/client/java/dev/spiritstudios/snapper/util/uploading/ScreenshotUploading.java @@ -2,9 +2,9 @@ import dev.spiritstudios.snapper.Snapper; import dev.spiritstudios.snapper.SnapperConfig; +import dev.spiritstudios.snapper.SnapperConstants; import dev.spiritstudios.snapper.gui.screen.PrivacyNoticeScreen; import dev.spiritstudios.snapper.util.SnapperUtil; -import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.MinecraftClient; import net.minecraft.client.toast.SystemToast; import net.minecraft.text.Text; @@ -14,16 +14,16 @@ 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") - .orElseThrow() - .getMetadata().getVersion().getFriendlyString(); + public static final String SNAPPER_VERSION = SnapperConstants.VERSION; private static final AxolotlClientApi API = new AxolotlClientApi(); + public static final SystemToast.Type SCREENSHOT_UPLOAD_TOAST = new SystemToast.Type(); + public static void toast(String title, String description, Object... args) { MinecraftClient.getInstance().getToastManager().add( SystemToast.create(MinecraftClient.getInstance(), - SystemToast.Type.WORLD_BACKUP, + SCREENSHOT_UPLOAD_TOAST, Text.translatable(title, args), Text.translatable(description, args))); } @@ -34,7 +34,7 @@ public static CompletableFuture upload(Path image) { return CompletableFuture.failedFuture(new IllegalStateException("Minecraft is currently running in offline mode.")); } - if (SnapperConfig.INSTANCE.termsAccepted.get() == AxolotlClientApi.TermsAcceptance.UNSET) { + if (SnapperConfig.termsAccepted == AxolotlClientApi.TermsAcceptance.UNSET) { MinecraftClient client = MinecraftClient.getInstance(); CompletableFuture success = new CompletableFuture<>(); @@ -45,7 +45,7 @@ public static CompletableFuture upload(Path image) { return success; } - if (SnapperConfig.INSTANCE.termsAccepted.get() != AxolotlClientApi.TermsAcceptance.ACCEPTED) { + if (SnapperConfig.termsAccepted != AxolotlClientApi.TermsAcceptance.ACCEPTED) { toast("toast.snapper.upload.failure", "toast.snapper.upload.axolotlclient.api_disabled"); return CompletableFuture.failedFuture(new IllegalStateException("AxolotlClient API is disabled.")); } diff --git a/src/client/resources/snapper.mixins.json b/src/client/resources/mixins.snapper.json similarity index 83% rename from src/client/resources/snapper.mixins.json rename to src/client/resources/mixins.snapper.json index dc69796..21d58f6 100644 --- a/src/client/resources/snapper.mixins.json +++ b/src/client/resources/mixins.snapper.json @@ -1,9 +1,10 @@ { "required": true, "package": "dev.spiritstudios.snapper.mixin", - "compatibilityLevel": "JAVA_21", + "compatibilityLevel": "${java_version}", "client": [ "CameraMixin", + "EntryListWidgetMixin", "GameMenuMixin", "KeyboardMixin", "MinecraftClientMixin", diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 37ca23b..359a21d 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -19,22 +19,17 @@ "environment": "client", "entrypoints": { "client": [ - "dev.spiritstudios.snapper.Snapper" - ], - "mixinsquared-adjuster": [ - "dev.spiritstudios.snapper.REMOVETHISAFTER1point21point5" + "dev.spiritstudios.snapper.SnapperEntrypoint" ] }, - "accessWidener": "snapper.accesswidener", "mixins": [ - "snapper.mixins.json" + "mixins.snapper.json" ], "depends": { - "fabricloader": ">=${fabric_loader_version}", - "minecraft": "${minecraft_version}", + "fabricloader": ">=0.15.11", + "minecraft": "${mc_version}", "fabric-api": "*", - "java": ">=21", - "specter-config": "*" + "java": ">=${java_version}" }, "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/versions/mainProject b/versions/mainProject new file mode 100644 index 0000000..685ed77 --- /dev/null +++ b/versions/mainProject @@ -0,0 +1 @@ +1.21.5-fabric \ No newline at end of file