From a6829261047c0df92a475cebe3ffca6456660805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?= Date: Thu, 15 Jan 2026 17:31:08 +0100 Subject: [PATCH] Improve "heart" ui --- .../service/ChannelDirectoryService.java | 1 + .../resources/static/css/customAssets.css | 56 +++++++++++++++---- src/main/resources/static/js/customAssets.js | 52 ++++++++++++----- 3 files changed, 84 insertions(+), 25 deletions(-) diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java b/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java index fc43dca..8a179f2 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java +++ b/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java @@ -619,6 +619,7 @@ public class ChannelDirectoryService { return applyMarketplaceHearts(entries, sessionUsername); } + @Transactional public Optional toggleMarketplaceHeart(String scriptId, String sessionUsername) { if (scriptId == null || scriptId.isBlank() || sessionUsername == null || sessionUsername.isBlank()) { return Optional.empty(); diff --git a/src/main/resources/static/css/customAssets.css b/src/main/resources/static/css/customAssets.css index 0f3e11a..f749554 100644 --- a/src/main/resources/static/css/customAssets.css +++ b/src/main/resources/static/css/customAssets.css @@ -302,17 +302,6 @@ white-space: nowrap; } -.modal .modal-inner .marketplace-hearts { - display: inline-flex; - align-items: center; - gap: 6px; - color: rgba(248, 113, 113, 0.9); -} - -.modal .modal-inner .marketplace-hearts i { - font-size: 12px; -} - .modal .modal-inner .marketplace-actions { display: flex; align-items: center; @@ -322,12 +311,57 @@ gap: 8px; } +.modal .modal-inner .marketplace-heart-count { + display: inline-flex; + align-items: center; + gap: 6px; + color: rgba(248, 113, 113, 0.9); + font-size: 12px; + font-weight: 600; +} + +.modal .modal-inner .marketplace-heart-count i { + font-size: 12px; +} + .modal .modal-inner .marketplace-heart-button.active { border-color: rgba(248, 113, 113, 0.5); background: rgba(248, 113, 113, 0.12); color: #fecdd3; } +.modal .modal-inner .marketplace-heart-button.is-animating .icon { + animation: marketplace-heart-pop 320ms ease; +} + +.modal .modal-inner .marketplace-heart-count.is-animating { + animation: marketplace-heart-count 260ms ease; +} + +@keyframes marketplace-heart-pop { + 0% { + transform: scale(1); + } + 45% { + transform: scale(1.25) rotate(-8deg); + } + 100% { + transform: scale(1); + } +} + +@keyframes marketplace-heart-count { + 0% { + transform: translateY(0); + } + 45% { + transform: translateY(-3px); + } + 100% { + transform: translateY(0); + } +} + .modal .modal-inner .marketplace-empty, .modal .modal-inner .marketplace-loading { padding: 14px; diff --git a/src/main/resources/static/js/customAssets.js b/src/main/resources/static/js/customAssets.js index 8b5c92a..51f5a6b 100644 --- a/src/main/resources/static/js/customAssets.js +++ b/src/main/resources/static/js/customAssets.js @@ -782,33 +782,39 @@ export function createCustomAssetModal({ description.textContent = entry.description || "No description provided."; const meta = document.createElement("small"); meta.textContent = entry.broadcaster ? `By ${entry.broadcaster}` : ""; - const hearts = document.createElement("small"); - hearts.className = "marketplace-hearts"; - const heartIcon = document.createElement("i"); - heartIcon.className = "fa-solid fa-heart"; - const heartCount = document.createElement("span"); - heartCount.textContent = String(entry.heartsCount ?? 0); - hearts.appendChild(heartIcon); - hearts.appendChild(heartCount); content.appendChild(title); content.appendChild(description); content.appendChild(meta); - content.appendChild(hearts); const actions = document.createElement("div"); actions.className = "marketplace-actions"; + const heartCountWrapper = document.createElement("div"); + heartCountWrapper.className = "marketplace-heart-count"; + const heartCountIcon = document.createElement("i"); + heartCountIcon.className = "fa-solid fa-heart"; + const heartCount = document.createElement("span"); + heartCount.textContent = String(entry.heartsCount ?? 0); + heartCountWrapper.appendChild(heartCountIcon); + heartCountWrapper.appendChild(heartCount); const heartButton = document.createElement("button"); heartButton.type = "button"; heartButton.className = "icon-button marketplace-heart-button"; heartButton.setAttribute("aria-label", "Heart script"); updateMarketplaceHeartButton(heartButton, entry); - heartButton.addEventListener("click", () => toggleMarketplaceHeart(entry, heartCount)); + heartButton.addEventListener("click", () => + toggleMarketplaceHeart(entry, { + button: heartButton, + count: heartCount, + countWrapper: heartCountWrapper, + }) + ); const importButton = document.createElement("button"); importButton.type = "button"; importButton.className = "icon-button"; importButton.setAttribute("aria-label", "Import script"); importButton.innerHTML = ''; importButton.addEventListener("click", () => importMarketplaceScript(entry)); + actions.appendChild(heartCountWrapper); actions.appendChild(heartButton); actions.appendChild(importButton); @@ -855,10 +861,11 @@ export function createCustomAssetModal({ button.innerHTML = ``; } - function toggleMarketplaceHeart(entry, countElement) { + function toggleMarketplaceHeart(entry, elements = {}) { if (!entry?.id) { return; } + animateMarketplaceHeart(elements); fetch(`/api/marketplace/scripts/${entry.id}/heart`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -872,10 +879,14 @@ export function createCustomAssetModal({ .then((updated) => { entry.heartsCount = updated.heartsCount ?? entry.heartsCount ?? 0; entry.hearted = updated.hearted ?? entry.hearted; - if (countElement) { - countElement.textContent = String(entry.heartsCount ?? 0); + if (elements.count) { + elements.count.textContent = String(entry.heartsCount ?? 0); } - renderMarketplace(); + if (elements.button) { + updateMarketplaceHeartButton(elements.button, entry); + } + animateMarketplaceHeart(elements); + setTimeout(() => renderMarketplace(), 300); }) .catch((error) => { console.error(error); @@ -883,6 +894,19 @@ export function createCustomAssetModal({ }); } + function animateMarketplaceHeart({ button, countWrapper } = {}) { + if (button) { + button.classList.remove("is-animating"); + void button.offsetWidth; + button.classList.add("is-animating"); + } + if (countWrapper) { + countWrapper.classList.remove("is-animating"); + void countWrapper.offsetWidth; + countWrapper.classList.add("is-animating"); + } + } + function debounce(fn, wait = 150) { let timeout; return (...args) => {