diff --git a/src/main/java/me/jellysquid/mods/sodium/client/SodiumClientMod.java b/src/main/java/me/jellysquid/mods/sodium/client/SodiumClientMod.java index d8fb562525..8c32d85f63 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/SodiumClientMod.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/SodiumClientMod.java @@ -111,7 +111,8 @@ private static void updateFingerprint() { if (saved == null || !current.looselyMatches(saved)) { HashedFingerprint.writeToDisk(current.hashed()); - CONFIG.notifications.hideDonationButton = false; + CONFIG.notifications.hasSeenDonationPrompt = false; + CONFIG.notifications.hasClearedDonationButton = false; try { UserConfig.writeToDisk(CONFIG); diff --git a/src/main/java/me/jellysquid/mods/sodium/client/data/config/UserConfig.java b/src/main/java/me/jellysquid/mods/sodium/client/data/config/UserConfig.java index 84ead0352b..fc373caa6d 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/data/config/UserConfig.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/data/config/UserConfig.java @@ -61,7 +61,10 @@ public static class QualitySettings { } public static class NotificationSettings { - public boolean hideDonationButton = false; + public boolean forceDisableDonationPrompts = false; + + public boolean hasClearedDonationButton = false; + public boolean hasSeenDonationPrompt = false; } public enum GraphicsQuality implements TextProvider { diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/RendererSettingsScreen.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/RendererSettingsScreen.java index 53e090d120..45e4beaf91 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/gui/RendererSettingsScreen.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/RendererSettingsScreen.java @@ -2,6 +2,7 @@ import me.jellysquid.mods.sodium.client.SodiumClientMod; import me.jellysquid.mods.sodium.client.data.config.UserConfig; +import me.jellysquid.mods.sodium.client.data.fingerprint.HashedFingerprint; import me.jellysquid.mods.sodium.client.gui.console.Console; import me.jellysquid.mods.sodium.client.gui.console.message.MessageLevel; import me.jellysquid.mods.sodium.client.gui.options.*; @@ -13,6 +14,7 @@ import me.jellysquid.mods.sodium.client.gui.screen.ConfigCorruptedScreen; import me.jellysquid.mods.sodium.client.gui.widgets.FlatButtonWidget; import me.jellysquid.mods.sodium.client.util.Dim2i; +import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; @@ -27,6 +29,8 @@ import org.lwjgl.glfw.GLFW; import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashSet; @@ -59,6 +63,61 @@ private RendererSettingsScreen(Screen prevScreen) { this.pages.add(RendererSettingsLayout.quality()); this.pages.add(RendererSettingsLayout.performance()); this.pages.add(RendererSettingsLayout.advanced()); + + this.checkPromptTimers(); + } + + private void checkPromptTimers() { + // Never show the prompt in developer workspaces. + if (FabricLoader.getInstance().isDevelopmentEnvironment()) { + return; + } + + var options = SodiumClientMod.options(); + + // If the user has disabled the nags forcefully (by config), or has already seen the prompt, don't show it again. + if (options.notifications.forceDisableDonationPrompts || options.notifications.hasSeenDonationPrompt) { + return; + } + + HashedFingerprint fingerprint = null; + + try { + fingerprint = HashedFingerprint.loadFromDisk(); + } catch (Throwable t) { + SodiumClientMod.logger() + .error("Failed to read the fingerprint from disk", t); + } + + // If the fingerprint doesn't exist, or failed to be loaded, abort. + if (fingerprint == null) { + return; + } + + // The fingerprint records the installation time. If it's been a while since installation, show the user + // a prompt asking for them to consider donating. + var now = Instant.now(); + var threshold = Instant.ofEpochSecond(fingerprint.timestamp()) + .plus(3, ChronoUnit.DAYS); + + if (now.isAfter(threshold)) { + this.openDonationPrompt(options); + } + } + + private void openDonationPrompt(UserConfig options) { + var prompt = new ScreenPrompt(this, DONATION_PROMPT_MESSAGE, 320, 190, + new ScreenPrompt.Action(Text.literal("Buy us a coffee"), this::openDonationPage)); + prompt.setFocused(true); + + options.notifications.hasSeenDonationPrompt = true; + + try { + UserConfig.writeToDisk(options); + } catch (IOException e) { + SodiumClientMod.logger() + .error("Failed to update config file", e); + } } public static Screen createScreen(Screen currentScreen) { @@ -105,7 +164,7 @@ private void rebuildGUI() { this.donateButton = new FlatButtonWidget(new Dim2i(this.width - 128, 6, 100, 20), Text.translatable("sodium.options.buttons.donate"), this::openDonationPage); this.hideDonateButton = new FlatButtonWidget(new Dim2i(this.width - 26, 6, 20, 20), Text.literal("x"), this::hideDonationButton); - if (SodiumClientMod.options().notifications.hideDonationButton) { + if (SodiumClientMod.options().notifications.hasClearedDonationButton || SodiumClientMod.options().notifications.forceDisableDonationPrompts) { this.setDonationButtonVisibility(false); } @@ -123,7 +182,7 @@ private void setDonationButtonVisibility(boolean value) { private void hideDonationButton() { UserConfig options = SodiumClientMod.options(); - options.notifications.hideDonationButton = true; + options.notifications.hasClearedDonationButton = true; try { UserConfig.writeToDisk(options); @@ -367,4 +426,16 @@ public ScreenPrompt getPrompt() { public Dim2i getDimensions() { return new Dim2i(0, 0, this.width, this.height); } + + private static final List DONATION_PROMPT_MESSAGE; + + static { + DONATION_PROMPT_MESSAGE = List.of( + StringVisitable.concat(Text.literal("Hello!")), + StringVisitable.concat(Text.literal("It seems that you've been enjoying "), Text.literal("Sodium").withColor(0x27eb92), Text.literal(", the free and open-source optimization mod for Minecraft.")), + StringVisitable.concat(Text.literal("Mods like these are complex. They require "), Text.literal("thousands of hours").withColor(0xff6e00), Text.literal(" of development, debugging, and tuning to create the experience that players have come to expect.")), + StringVisitable.concat(Text.literal("If you'd like to show your token of appreciation, and support the development of our mod in the process, then consider "), Text.literal("buying us a coffee").withColor(0xed49ce), Text.literal(".")), + StringVisitable.concat(Text.literal("And thanks again for using our mod! We hope it helps you (and your computer.)")) + ); + } } diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/prompt/ScreenPrompt.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/prompt/ScreenPrompt.java index c62e6489a0..e00e8a073b 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/gui/prompt/ScreenPrompt.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/prompt/ScreenPrompt.java @@ -9,7 +9,6 @@ import net.minecraft.client.gui.Element; import net.minecraft.text.StringVisitable; import net.minecraft.text.Text; -import net.minecraft.util.Util; import org.jetbrains.annotations.NotNull; import org.lwjgl.glfw.GLFW; @@ -19,16 +18,20 @@ public class ScreenPrompt implements Element, Drawable { private final ScreenPromptable parent; private final List text; + private final Action action; + private FlatButtonWidget closeButton, actionButton; private final int width, height; - public ScreenPrompt(ScreenPromptable parent, List text, int width, int height) { + public ScreenPrompt(ScreenPromptable parent, List text, int width, int height, Action action) { this.parent = parent; this.text = text; this.width = width; this.height = height; + + this.action = action; } public void render(DrawContext drawContext, int mouseX, int mouseY, float delta) { @@ -64,7 +67,7 @@ public void render(DrawContext drawContext, int mouseX, int mouseY, float delta) var formatted = textRenderer.wrapLines(paragraph, textMaxWidth); for (var line : formatted) { - drawContext.drawText(textRenderer, line, textX, textY, 0x0, true); + drawContext.drawText(textRenderer, line, textX, textY, 0xFFFFFFFF, true); textY += textRenderer.fontHeight + 2; } @@ -74,7 +77,7 @@ public void render(DrawContext drawContext, int mouseX, int mouseY, float delta) this.closeButton = new FlatButtonWidget(new Dim2i((boxX + width) - 84, (boxY + height) - 24, 80, 20), Text.literal("Close"), this::close); this.closeButton.setStyle(createButtonStyle()); - this.actionButton = new FlatButtonWidget(new Dim2i((boxX + width) - 198, (boxY + height) - 24, 110, 20), Text.literal("Perform action"), this::close); + this.actionButton = new FlatButtonWidget(new Dim2i((boxX + width) - 198, (boxY + height) - 24, 110, 20), this.action.label, this::runAction); this.actionButton.setStyle(createButtonStyle()); for (var button : getWidgets()) { @@ -138,4 +141,13 @@ public boolean isFocused() { private void close() { this.parent.setPrompt(null); } + + private void runAction() { + this.action.runnable.run(); + this.close(); + } + + public record Action(Text label, Runnable runnable) { + + } }