diff --git a/src/main/java/com/toxicstoxm/LEDSuite/tools/YamlTools.java b/src/main/java/com/toxicstoxm/LEDSuite/tools/YamlTools.java index a17412c5..54eb21ed 100644 --- a/src/main/java/com/toxicstoxm/LEDSuite/tools/YamlTools.java +++ b/src/main/java/com/toxicstoxm/LEDSuite/tools/YamlTools.java @@ -2,6 +2,7 @@ import com.toxicstoxm.LEDSuite.communication.packet_management.DeserializationException; import com.toxicstoxm.LEDSuite.communication.packet_management.packets.errors.ErrorCode; +import com.toxicstoxm.LEDSuite.ui.HashSumCallable; import com.toxicstoxm.YAJL.Logger; import com.toxicstoxm.YAJSI.api.yaml.ConfigurationSection; import io.github.jwharm.javagi.base.GErrorException; @@ -12,6 +13,7 @@ import org.jetbrains.annotations.NotNull; import java.util.Base64; +import java.util.Objects; /** * A utility class providing methods for interacting with YAML configuration sections. @@ -147,13 +149,16 @@ public static long getLongIfAvailable(String key, long defaultValue, @NotNull Co * @param iconIsName true if {@code iconString} should be treated as name, otherwise {@code false} * @return the constructed {@link Image} or a 'broken image' if something went wrong or the name/base64 was invalid. */ - public static Image constructIcon(String iconString, boolean iconIsName) { + public static Image constructIcon(String iconString, boolean iconIsName, String iconHash, HashSumCallable newIcon) { Image finalImage = Image.fromIconName(""); if (iconIsName) { finalImage = Image.fromIconName(iconString); } else { - byte[] decodedBytes = Base64.getDecoder().decode(iconString); try { + if (Objects.equals(newIcon.getHashSum(), iconHash)) return null; + + byte[] decodedBytes = Base64.getDecoder().decode(iconString); + finalImage = Image.fromPaintable(Texture.fromBytes(Bytes.static_(decodedBytes))); } catch (GErrorException e) { logger.warn("Failed to decode icon from base64! Error message: '{}'!", e.getMessage()); diff --git a/src/main/java/com/toxicstoxm/LEDSuite/ui/AnimationRow.java b/src/main/java/com/toxicstoxm/LEDSuite/ui/AnimationRow.java index d8741fa7..24dec29a 100644 --- a/src/main/java/com/toxicstoxm/LEDSuite/ui/AnimationRow.java +++ b/src/main/java/com/toxicstoxm/LEDSuite/ui/AnimationRow.java @@ -2,7 +2,9 @@ import com.toxicstoxm.LEDSuite.communication.packet_management.packets.requests.MenuRequestPacket; import com.toxicstoxm.LEDSuite.time.CooldownManager; +import com.toxicstoxm.LEDSuite.tools.YamlTools; import com.toxicstoxm.LEDSuite.ui.animation_menu.AnimationMenuReference; +import com.toxicstoxm.LEDSuite.ui.dialogs.alert_dialogs.ErrorData; import com.toxicstoxm.YAJL.Logger; import io.github.jwharm.javagi.gtk.annotations.GtkChild; import io.github.jwharm.javagi.gtk.annotations.GtkTemplate; @@ -18,6 +20,10 @@ import org.jetbrains.annotations.NotNull; import java.lang.foreign.MemorySegment; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.atomic.AtomicReference; /** * Represents a row in the sidebar of the application, displaying information about a single animation. @@ -66,19 +72,7 @@ public AnimationRow(MemorySegment address) { @GtkChild(name = "animation_icon") public Image animationIcon; - /** - * Sets the animation icons paintable or icon name depending on the provided icons structure. - * - * @param icon the new icon to use - */ - public final void setIcon(@NotNull Image icon) { - ImageType storageType = icon.getStorageType(); - if (storageType == ImageType.EMPTY || storageType == ImageType.ICON_NAME) { - animationIcon.setFromIconName(icon.getIconName()); - } else { - animationIcon.setFromPaintable(icon.getPaintable()); - } - } + private String iconHash = ""; /** * The {@link Label} widget used to display the animation's label. @@ -121,7 +115,7 @@ public final void setAnimationLabel(String animationLabel) { // Instantiate and configure the AnimationRow with the provided data. AnimationRow row = GObject.newInstance(AnimationRow.class, "action-name", "app." + animationRowData.animationID()); row.animationID = animationRowData.animationID(); - row.setIcon(animationRowData.icon()); + row.update(null, animationRowData.iconString(), animationRowData.iconIsName(), null); row.setLastAccessed(animationRowData.lastAccessed()); row.setAnimationLabel(animationRowData.label().strip()); row.animationRowLabel.setWrap(true); // Allow label to wrap if it's too long @@ -138,18 +132,38 @@ public final void setAnimationLabel(String animationLabel) { * This method can be called to refresh the row's display. * * @param label The new label to display. - * @param iconName The new icon name to display. + * @param iconString The new icon string. * @param lastAccessed The last time this animation was accessed */ - public void update(String label, String iconName, Long lastAccessed) { - animationRowLabel.setLabel(label); - animationIcon.setFromIconName(iconName); - setLastAccessed(lastAccessed); + public void update(String label, String iconString, boolean isName, Long lastAccessed) { + if (label != null) animationRowLabel.setLabel(label); + AtomicReference newIconHash = new AtomicReference<>(""); + Image newIcon = YamlTools.constructIcon(iconString, isName, iconHash, () -> { + try { + newIconHash.set(new String(MessageDigest.getInstance("md5").digest(iconString.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8)); + } catch (NoSuchAlgorithmException e) { + LEDSuiteApplication.handleError(ErrorData.builder() + .logger(logger) + .message(logger.format("Failed to get hashSum algorithm 'md5' for computing icon hashSum within {}(ID:{})", getClass().getSimpleName(), getAnimationID())) + .heading("Error while receiving status update") + .build()); + } + return newIconHash.get(); + }); + if (newIcon != null) { + if (isName) { + animationIcon.setFromIconName(newIcon.getIconName()); + } else { + animationIcon.setFromPaintable(newIcon.getPaintable()); + } + iconHash = newIconHash.get(); + } + if (lastAccessed != null) setLastAccessed(lastAccessed); // If the row is part of a menu, update the menu as well. if (animationMenuReference != null) { animationMenuReference.updateLabel(label); - animationMenuReference.updateIconName(iconName); + animationMenuReference.updateIcon(newIcon); } } diff --git a/src/main/java/com/toxicstoxm/LEDSuite/ui/AnimationRowData.java b/src/main/java/com/toxicstoxm/LEDSuite/ui/AnimationRowData.java index d7575b0d..a1e04c55 100644 --- a/src/main/java/com/toxicstoxm/LEDSuite/ui/AnimationRowData.java +++ b/src/main/java/com/toxicstoxm/LEDSuite/ui/AnimationRowData.java @@ -3,7 +3,6 @@ import com.toxicstoxm.LEDSuite.time.Action; import lombok.Builder; import org.gnome.gtk.Application; -import org.gnome.gtk.Image; import org.jetbrains.annotations.NotNull; /** @@ -17,7 +16,8 @@ * @since 1.0.0 * * @param app The main {@link Application} instance for the GTK application. This is used to associate the row with the application. - * @param icon The GTK name or base64 encoded image file of the icon to represent the animation. + * @param iconString The GTK name or base64 encoded image file of the icon to represent the animation. + * @param iconIsName If the above param is base64 or an icon name. * @param label The name or description of the animation. This text is displayed on the animation row. * @param animationID The unique identifier for the animation. This could be the animation's ID or the filename associated with it. * @param action The action associated with this animation, typically used to handle interactions like button clicks. This is typically a unique action name used to trigger specific behavior. @@ -26,7 +26,8 @@ @Builder public record AnimationRowData( @NotNull Application app, - Image icon, + String iconString, + boolean iconIsName, String label, String animationID, Action action, diff --git a/src/main/java/com/toxicstoxm/LEDSuite/ui/HashSumCallable.java b/src/main/java/com/toxicstoxm/LEDSuite/ui/HashSumCallable.java new file mode 100644 index 00000000..deef9fa1 --- /dev/null +++ b/src/main/java/com/toxicstoxm/LEDSuite/ui/HashSumCallable.java @@ -0,0 +1,6 @@ +package com.toxicstoxm.LEDSuite.ui; + +@FunctionalInterface +public interface HashSumCallable { + String getHashSum(); +} diff --git a/src/main/java/com/toxicstoxm/LEDSuite/ui/LEDSuiteWindow.java b/src/main/java/com/toxicstoxm/LEDSuite/ui/LEDSuiteWindow.java index 7041bb74..c688908e 100644 --- a/src/main/java/com/toxicstoxm/LEDSuite/ui/LEDSuiteWindow.java +++ b/src/main/java/com/toxicstoxm/LEDSuite/ui/LEDSuiteWindow.java @@ -12,7 +12,6 @@ import com.toxicstoxm.LEDSuite.formatting.StringFormatter; import com.toxicstoxm.LEDSuite.gettext.Translations; import com.toxicstoxm.LEDSuite.task_scheduler.LEDSuiteRunnable; -import com.toxicstoxm.LEDSuite.tools.YamlTools; import com.toxicstoxm.LEDSuite.ui.animation_menu.AnimationMenu; import com.toxicstoxm.LEDSuite.ui.dialogs.alert_dialogs.FileCollisionDialog; import com.toxicstoxm.LEDSuite.ui.dialogs.alert_dialogs.OverwriteConfirmationDialog; @@ -316,7 +315,7 @@ public void updateAnimations(@NotNull Collection up // Update the animation row with new data GLib.idleAddOnce(() -> { - animationRow.update(updatedAnimation.label(), updatedAnimation.iconString(), updatedAnimation.lastAccessed()); + animationRow.update(updatedAnimation.label(), updatedAnimation.iconString(), updatedAnimation.iconIsName(), updatedAnimation.lastAccessed()); logger.verbose("Updated animation: {}", newAnimationName); }); @@ -333,7 +332,8 @@ public void updateAnimations(@NotNull Collection up var newAnimationRow = AnimationRow.create( AnimationRowData.builder() .app(getApplication()) - .icon(YamlTools.constructIcon(updatedAnimation.iconString(), updatedAnimation.iconIsName())) + .iconString(updatedAnimation.iconString()) + .iconIsName(updatedAnimation.iconIsName()) .label(updatedAnimation.label()) .animationID(updatedAnimation.id()) .cooldown(500L) diff --git a/src/main/java/com/toxicstoxm/LEDSuite/ui/animation_menu/AnimationMenu.java b/src/main/java/com/toxicstoxm/LEDSuite/ui/animation_menu/AnimationMenu.java index 234425db..a63ca536 100644 --- a/src/main/java/com/toxicstoxm/LEDSuite/ui/animation_menu/AnimationMenu.java +++ b/src/main/java/com/toxicstoxm/LEDSuite/ui/animation_menu/AnimationMenu.java @@ -121,10 +121,15 @@ public void updateLabel(String label) { } @Override - public void updateIconName(String iconName) { + public void updateIcon(Image icon) { GLib.idleAddOnce(() -> { - if (iconName != null) { - animationMenuImage.setFromIconName(iconName); + if (icon != null) { + ImageType storageType = icon.getStorageType(); + if (storageType == ImageType.EMPTY || storageType == ImageType.ICON_NAME) { + animationMenuImage.setFromIconName(icon.getIconName()); + } else { + animationMenuImage.setFromPaintable(icon.getPaintable()); + } } }); } diff --git a/src/main/java/com/toxicstoxm/LEDSuite/ui/animation_menu/AnimationMenuReference.java b/src/main/java/com/toxicstoxm/LEDSuite/ui/animation_menu/AnimationMenuReference.java index afa17b54..983e6ae9 100644 --- a/src/main/java/com/toxicstoxm/LEDSuite/ui/animation_menu/AnimationMenuReference.java +++ b/src/main/java/com/toxicstoxm/LEDSuite/ui/animation_menu/AnimationMenuReference.java @@ -1,10 +1,12 @@ package com.toxicstoxm.LEDSuite.ui.animation_menu; +import org.gnome.gtk.Image; + /** * Add interface to hold reference to an {@link AnimationMenu}'s update methods. * @since 1.0.0 */ public interface AnimationMenuReference { void updateLabel(String label); - void updateIconName(String iconName); + void updateIcon(Image iconName); }