diff --git a/src/main/java/dev/kruhlmann/imgfloat/config/SchemaMigration.java b/src/main/java/dev/kruhlmann/imgfloat/config/SchemaMigration.java index 874c6bc..d2bdab9 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/config/SchemaMigration.java +++ b/src/main/java/dev/kruhlmann/imgfloat/config/SchemaMigration.java @@ -25,6 +25,7 @@ public class SchemaMigration implements ApplicationRunner { ensureSessionAttributeUpsertTrigger(); ensureChannelCanvasColumns(); ensureAssetTables(); + ensureMarketplaceScriptHeartsTable(); ensureAuthorizedClientTable(); normalizeAuthorizedClientTimestamps(); } @@ -169,6 +170,22 @@ public class SchemaMigration implements ApplicationRunner { } } + private void ensureMarketplaceScriptHeartsTable() { + try { + jdbcTemplate.execute( + """ + CREATE TABLE IF NOT EXISTS marketplace_script_hearts ( + script_id TEXT NOT NULL, + username TEXT NOT NULL, + PRIMARY KEY (script_id, username) + ) + """ + ); + } catch (DataAccessException ex) { + logger.warn("Unable to ensure marketplace script hearts table", ex); + } + } + private void ensureScriptAssetColumns() { List scriptColumns; List attachmentColumns; diff --git a/src/main/java/dev/kruhlmann/imgfloat/controller/ScriptMarketplaceController.java b/src/main/java/dev/kruhlmann/imgfloat/controller/ScriptMarketplaceController.java index 73732d4..655c30a 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/controller/ScriptMarketplaceController.java +++ b/src/main/java/dev/kruhlmann/imgfloat/controller/ScriptMarketplaceController.java @@ -46,8 +46,12 @@ public class ScriptMarketplaceController { } @GetMapping("/scripts") - public List listMarketplaceScripts(@RequestParam(value = "query", required = false) String query) { - return channelDirectoryService.listMarketplaceScripts(query); + public List listMarketplaceScripts( + @RequestParam(value = "query", required = false) String query, + OAuth2AuthenticationToken oauthToken + ) { + String sessionUsername = oauthToken == null ? null : OauthSessionUser.from(oauthToken).login(); + return channelDirectoryService.listMarketplaceScripts(query, sessionUsername); } @GetMapping("/scripts/{scriptId}/logo") @@ -85,4 +89,16 @@ public class ScriptMarketplaceController { .map(ResponseEntity::ok) .orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Unable to import script")); } + + @PostMapping("/scripts/{scriptId}/heart") + public ResponseEntity toggleMarketplaceHeart( + @PathVariable("scriptId") String scriptId, + OAuth2AuthenticationToken oauthToken + ) { + String sessionUsername = OauthSessionUser.from(oauthToken).login(); + return channelDirectoryService + .toggleMarketplaceHeart(scriptId, sessionUsername) + .map(ResponseEntity::ok) + .orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Marketplace script not found")); + } } diff --git a/src/main/java/dev/kruhlmann/imgfloat/model/MarketplaceScriptHeart.java b/src/main/java/dev/kruhlmann/imgfloat/model/MarketplaceScriptHeart.java new file mode 100644 index 0000000..8eb6615 --- /dev/null +++ b/src/main/java/dev/kruhlmann/imgfloat/model/MarketplaceScriptHeart.java @@ -0,0 +1,44 @@ +package dev.kruhlmann.imgfloat.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; + +@Entity +@Table(name = "marketplace_script_hearts") +@IdClass(MarketplaceScriptHeartId.class) +public class MarketplaceScriptHeart { + + @Id + @Column(name = "script_id") + private String scriptId; + + @Id + @Column(name = "username") + private String username; + + public MarketplaceScriptHeart() {} + + public MarketplaceScriptHeart(String scriptId, String username) { + this.scriptId = scriptId; + this.username = username; + } + + public String getScriptId() { + return scriptId; + } + + public void setScriptId(String scriptId) { + this.scriptId = scriptId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } +} diff --git a/src/main/java/dev/kruhlmann/imgfloat/model/MarketplaceScriptHeartId.java b/src/main/java/dev/kruhlmann/imgfloat/model/MarketplaceScriptHeartId.java new file mode 100644 index 0000000..abf1d82 --- /dev/null +++ b/src/main/java/dev/kruhlmann/imgfloat/model/MarketplaceScriptHeartId.java @@ -0,0 +1,50 @@ +package dev.kruhlmann.imgfloat.model; + +import java.io.Serializable; +import java.util.Objects; + +public class MarketplaceScriptHeartId implements Serializable { + + private String scriptId; + private String username; + + public MarketplaceScriptHeartId() {} + + public MarketplaceScriptHeartId(String scriptId, String username) { + this.scriptId = scriptId; + this.username = username; + } + + public String getScriptId() { + return scriptId; + } + + public void setScriptId(String scriptId) { + this.scriptId = scriptId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + MarketplaceScriptHeartId that = (MarketplaceScriptHeartId) other; + return Objects.equals(scriptId, that.scriptId) && Objects.equals(username, that.username); + } + + @Override + public int hashCode() { + return Objects.hash(scriptId, username); + } +} diff --git a/src/main/java/dev/kruhlmann/imgfloat/model/ScriptMarketplaceEntry.java b/src/main/java/dev/kruhlmann/imgfloat/model/ScriptMarketplaceEntry.java index d59f43b..3ae58e4 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/model/ScriptMarketplaceEntry.java +++ b/src/main/java/dev/kruhlmann/imgfloat/model/ScriptMarketplaceEntry.java @@ -5,5 +5,7 @@ public record ScriptMarketplaceEntry( String name, String description, String logoUrl, - String broadcaster + String broadcaster, + long heartsCount, + boolean hearted ) {} diff --git a/src/main/java/dev/kruhlmann/imgfloat/repository/MarketplaceScriptHeartRepository.java b/src/main/java/dev/kruhlmann/imgfloat/repository/MarketplaceScriptHeartRepository.java new file mode 100644 index 0000000..1c26d34 --- /dev/null +++ b/src/main/java/dev/kruhlmann/imgfloat/repository/MarketplaceScriptHeartRepository.java @@ -0,0 +1,35 @@ +package dev.kruhlmann.imgfloat.repository; + +import dev.kruhlmann.imgfloat.model.MarketplaceScriptHeart; +import dev.kruhlmann.imgfloat.model.MarketplaceScriptHeartId; +import java.util.Collection; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface MarketplaceScriptHeartRepository + extends JpaRepository { + interface ScriptHeartCount { + String getScriptId(); + long getHeartCount(); + } + + @Query( + """ + SELECT h.scriptId AS scriptId, COUNT(h) AS heartCount + FROM MarketplaceScriptHeart h + WHERE h.scriptId IN :scriptIds + GROUP BY h.scriptId + """ + ) + List countByScriptIds(@Param("scriptIds") Collection scriptIds); + + List findByUsernameAndScriptIdIn(String username, Collection scriptIds); + + boolean existsByScriptIdAndUsername(String scriptId, String username); + + long countByScriptId(String scriptId); + + void deleteByScriptIdAndUsername(String scriptId, String username); +} diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java b/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java index 19ff5eb..8ccdce6 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java +++ b/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java @@ -13,6 +13,7 @@ import dev.kruhlmann.imgfloat.model.CanvasEvent; import dev.kruhlmann.imgfloat.model.CanvasSettingsRequest; import dev.kruhlmann.imgfloat.model.Channel; import dev.kruhlmann.imgfloat.model.CodeAssetRequest; +import dev.kruhlmann.imgfloat.model.MarketplaceScriptHeart; import dev.kruhlmann.imgfloat.model.PlaybackRequest; import dev.kruhlmann.imgfloat.model.ScriptAsset; import dev.kruhlmann.imgfloat.model.ScriptAssetAttachment; @@ -26,6 +27,7 @@ import dev.kruhlmann.imgfloat.model.VisualAsset; import dev.kruhlmann.imgfloat.repository.AssetRepository; import dev.kruhlmann.imgfloat.repository.AudioAssetRepository; import dev.kruhlmann.imgfloat.repository.ChannelRepository; +import dev.kruhlmann.imgfloat.repository.MarketplaceScriptHeartRepository; import dev.kruhlmann.imgfloat.repository.ScriptAssetRepository; import dev.kruhlmann.imgfloat.repository.ScriptAssetAttachmentRepository; import dev.kruhlmann.imgfloat.repository.ScriptAssetFileRepository; @@ -66,6 +68,7 @@ public class ChannelDirectoryService { private final ScriptAssetRepository scriptAssetRepository; private final ScriptAssetAttachmentRepository scriptAssetAttachmentRepository; private final ScriptAssetFileRepository scriptAssetFileRepository; + private final MarketplaceScriptHeartRepository marketplaceScriptHeartRepository; private final SimpMessagingTemplate messagingTemplate; private final AssetStorageService assetStorageService; private final MediaDetectionService mediaDetectionService; @@ -83,6 +86,7 @@ public class ChannelDirectoryService { ScriptAssetRepository scriptAssetRepository, ScriptAssetAttachmentRepository scriptAssetAttachmentRepository, ScriptAssetFileRepository scriptAssetFileRepository, + MarketplaceScriptHeartRepository marketplaceScriptHeartRepository, SimpMessagingTemplate messagingTemplate, AssetStorageService assetStorageService, MediaDetectionService mediaDetectionService, @@ -98,6 +102,7 @@ public class ChannelDirectoryService { this.scriptAssetRepository = scriptAssetRepository; this.scriptAssetAttachmentRepository = scriptAssetAttachmentRepository; this.scriptAssetFileRepository = scriptAssetFileRepository; + this.marketplaceScriptHeartRepository = marketplaceScriptHeartRepository; this.messagingTemplate = messagingTemplate; this.assetStorageService = assetStorageService; this.mediaDetectionService = mediaDetectionService; @@ -430,7 +435,7 @@ public class ChannelDirectoryService { return Optional.of(view); } - public List listMarketplaceScripts(String query) { + public List listMarketplaceScripts(String query, String sessionUsername) { String q = normalizeDescription(query); String normalizedQuery = q == null ? null : q.toLowerCase(Locale.ROOT); List entries = new ArrayList<>(marketplaceScriptSeedLoader.listEntriesForQuery(normalizedQuery)); @@ -439,12 +444,7 @@ public class ChannelDirectoryService { scripts = scriptAssetRepository.findByIsPublicTrue(); } catch (DataAccessException ex) { logger.warn("Unable to load marketplace scripts", ex); - return entries - .stream() - .sorted( - Comparator.comparing(ScriptMarketplaceEntry::name, Comparator.nullsLast(String::compareToIgnoreCase)) - ) - .toList(); + return applyMarketplaceHearts(entries, sessionUsername); } if (normalizedQuery != null && !normalizedQuery.isBlank()) { scripts = @@ -477,15 +477,145 @@ public class ChannelDirectoryService { script.getName(), script.getDescription(), logoUrl, - broadcaster + broadcaster, + 0, + false ); }) .toList() ); + return applyMarketplaceHearts(entries, sessionUsername); + } + + public Optional toggleMarketplaceHeart(String scriptId, String sessionUsername) { + if (scriptId == null || scriptId.isBlank() || sessionUsername == null || sessionUsername.isBlank()) { + return Optional.empty(); + } + try { + if (marketplaceScriptHeartRepository.existsByScriptIdAndUsername(scriptId, sessionUsername)) { + marketplaceScriptHeartRepository.deleteByScriptIdAndUsername(scriptId, sessionUsername); + } else { + marketplaceScriptHeartRepository.save(new MarketplaceScriptHeart(scriptId, sessionUsername)); + } + } catch (DataAccessException ex) { + logger.warn("Unable to update marketplace heart for {}", scriptId, ex); + return Optional.empty(); + } + return loadMarketplaceEntryWithHearts(scriptId, sessionUsername); + } + + private Optional loadMarketplaceEntryWithHearts(String scriptId, String sessionUsername) { + Optional seedScript = marketplaceScriptSeedLoader.findById(scriptId); + if (seedScript.isPresent()) { + return Optional.of(applyMarketplaceHearts(seedScript.get().entry(), sessionUsername)); + } + ScriptAsset script; + try { + script = scriptAssetRepository.findById(scriptId).filter(ScriptAsset::isPublic).orElse(null); + } catch (DataAccessException ex) { + logger.warn("Unable to load marketplace script {}", scriptId, ex); + return Optional.empty(); + } + if (script == null) { + return Optional.empty(); + } + Asset asset = assetRepository.findById(scriptId).orElse(null); + String broadcaster = asset != null ? asset.getBroadcaster() : ""; + String logoUrl = script.getLogoFileId() == null ? null : "/api/marketplace/scripts/" + script.getId() + "/logo"; + ScriptMarketplaceEntry entry = new ScriptMarketplaceEntry( + script.getId(), + script.getName(), + script.getDescription(), + logoUrl, + broadcaster, + 0, + false + ); + return Optional.of(applyMarketplaceHearts(entry, sessionUsername)); + } + + private ScriptMarketplaceEntry applyMarketplaceHearts(ScriptMarketplaceEntry entry, String sessionUsername) { + if (entry == null || entry.id() == null) { + return entry; + } + long heartCount; + boolean hearted; + try { + heartCount = marketplaceScriptHeartRepository.countByScriptId(entry.id()); + hearted = + sessionUsername != null && + marketplaceScriptHeartRepository.existsByScriptIdAndUsername(entry.id(), sessionUsername); + } catch (DataAccessException ex) { + logger.warn("Unable to load marketplace heart summary for {}", entry.id(), ex); + heartCount = 0; + hearted = false; + } + return new ScriptMarketplaceEntry( + entry.id(), + entry.name(), + entry.description(), + entry.logoUrl(), + entry.broadcaster(), + heartCount, + hearted + ); + } + + private List applyMarketplaceHearts( + List entries, + String sessionUsername + ) { + if (entries == null || entries.isEmpty()) { + return List.of(); + } + List scriptIds = entries.stream().map(ScriptMarketplaceEntry::id).filter(Objects::nonNull).toList(); + Map counts = new HashMap<>(); + Set heartedIds = new HashSet<>(); + try { + if (!scriptIds.isEmpty()) { + counts.putAll( + marketplaceScriptHeartRepository + .countByScriptIds(scriptIds) + .stream() + .collect( + Collectors.toMap( + MarketplaceScriptHeartRepository.ScriptHeartCount::getScriptId, + MarketplaceScriptHeartRepository.ScriptHeartCount::getHeartCount + ) + ) + ); + if (sessionUsername != null && !sessionUsername.isBlank()) { + heartedIds.addAll( + marketplaceScriptHeartRepository + .findByUsernameAndScriptIdIn(sessionUsername, scriptIds) + .stream() + .map(MarketplaceScriptHeart::getScriptId) + .collect(Collectors.toSet()) + ); + } + } + } catch (DataAccessException ex) { + logger.warn("Unable to load marketplace heart summaries", ex); + } + Comparator comparator = Comparator + .comparingLong((ScriptMarketplaceEntry entry) -> counts.getOrDefault(entry.id(), 0L)) + .reversed() + .thenComparing(ScriptMarketplaceEntry::name, Comparator.nullsLast(String::compareToIgnoreCase)); return entries .stream() - .sorted(Comparator.comparing(ScriptMarketplaceEntry::name, Comparator.nullsLast(String::compareToIgnoreCase))) + .map((entry) -> + new ScriptMarketplaceEntry( + entry.id(), + entry.name(), + entry.description(), + entry.logoUrl(), + entry.broadcaster(), + counts.getOrDefault(entry.id(), 0L), + heartedIds.contains(entry.id()) + ) + ) + .sorted(comparator) .toList(); } diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/MarketplaceScriptSeedLoader.java b/src/main/java/dev/kruhlmann/imgfloat/service/MarketplaceScriptSeedLoader.java index 4863b08..036f1be 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/service/MarketplaceScriptSeedLoader.java +++ b/src/main/java/dev/kruhlmann/imgfloat/service/MarketplaceScriptSeedLoader.java @@ -86,7 +86,9 @@ public class MarketplaceScriptSeedLoader { name, description, logoPath.isPresent() ? "/api/marketplace/scripts/" + id + "/logo" : null, - broadcaster + broadcaster, + 0, + false ); } diff --git a/src/main/resources/db/migration/V2__marketplace_hearts.sql b/src/main/resources/db/migration/V2__marketplace_hearts.sql new file mode 100644 index 0000000..68434a7 --- /dev/null +++ b/src/main/resources/db/migration/V2__marketplace_hearts.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS marketplace_script_hearts ( + script_id TEXT NOT NULL, + username TEXT NOT NULL, + PRIMARY KEY (script_id, username) +); diff --git a/src/main/resources/static/css/customAssets.css b/src/main/resources/static/css/customAssets.css index 8989b4d..0f3e11a 100644 --- a/src/main/resources/static/css/customAssets.css +++ b/src/main/resources/static/css/customAssets.css @@ -302,12 +302,30 @@ 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; margin-top: auto; width: 100%; justify-content: flex-end; + gap: 8px; +} + +.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-empty, diff --git a/src/main/resources/static/js/customAssets.js b/src/main/resources/static/js/customAssets.js index abfb85b..882f537 100644 --- a/src/main/resources/static/js/customAssets.js +++ b/src/main/resources/static/js/customAssets.js @@ -750,7 +750,14 @@ export function createCustomAssetModal({ marketplaceList.innerHTML = '
No scripts found.
'; return; } - marketplaceEntries.forEach((entry) => { + const sortedEntries = [...marketplaceEntries].sort((a, b) => { + const heartsDelta = (b.heartsCount ?? 0) - (a.heartsCount ?? 0); + if (heartsDelta !== 0) { + return heartsDelta; + } + return (a.name || "").localeCompare(b.name || "", undefined, { sensitivity: "base" }); + }); + sortedEntries.forEach((entry) => { const card = document.createElement("div"); card.className = "marketplace-card"; @@ -775,17 +782,34 @@ 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 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)); const importButton = document.createElement("button"); importButton.type = "button"; - importButton.className = "primary"; - importButton.textContent = "Import"; + importButton.className = "icon-button"; + importButton.setAttribute("aria-label", "Import script"); + importButton.innerHTML = ''; importButton.addEventListener("click", () => importMarketplaceScript(entry)); + actions.appendChild(heartButton); actions.appendChild(importButton); card.appendChild(content); @@ -821,6 +845,44 @@ export function createCustomAssetModal({ }); } + function updateMarketplaceHeartButton(button, entry) { + if (!button || !entry) { + return; + } + button.classList.toggle("active", Boolean(entry.hearted)); + button.setAttribute("aria-pressed", entry.hearted ? "true" : "false"); + const iconClass = entry.hearted ? "fa-solid fa-heart" : "fa-regular fa-heart"; + button.innerHTML = ``; + } + + function toggleMarketplaceHeart(entry, countElement) { + if (!entry?.id) { + return; + } + fetch(`/api/marketplace/scripts/${entry.id}/heart`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + }) + .then((response) => { + if (!response.ok) { + throw new Error("Failed to update heart"); + } + return response.json(); + }) + .then((updated) => { + entry.heartsCount = updated.heartsCount ?? entry.heartsCount ?? 0; + entry.hearted = updated.hearted ?? entry.hearted; + if (countElement) { + countElement.textContent = String(entry.heartsCount ?? 0); + } + renderMarketplace(); + }) + .catch((error) => { + console.error(error); + showToast?.("Unable to update heart. Please try again.", "error"); + }); + } + function debounce(fn, wait = 150) { let timeout; return (...args) => { diff --git a/src/test/java/dev/kruhlmann/imgfloat/ChannelDirectoryServiceTest.java b/src/test/java/dev/kruhlmann/imgfloat/ChannelDirectoryServiceTest.java index 3e98db3..d1947bf 100644 --- a/src/test/java/dev/kruhlmann/imgfloat/ChannelDirectoryServiceTest.java +++ b/src/test/java/dev/kruhlmann/imgfloat/ChannelDirectoryServiceTest.java @@ -20,6 +20,7 @@ import dev.kruhlmann.imgfloat.model.VisibilityRequest; import dev.kruhlmann.imgfloat.repository.AssetRepository; import dev.kruhlmann.imgfloat.repository.AudioAssetRepository; import dev.kruhlmann.imgfloat.repository.ChannelRepository; +import dev.kruhlmann.imgfloat.repository.MarketplaceScriptHeartRepository; import dev.kruhlmann.imgfloat.repository.ScriptAssetRepository; import dev.kruhlmann.imgfloat.repository.ScriptAssetAttachmentRepository; import dev.kruhlmann.imgfloat.repository.ScriptAssetFileRepository; @@ -61,6 +62,7 @@ class ChannelDirectoryServiceTest { private ScriptAssetRepository scriptAssetRepository; private ScriptAssetAttachmentRepository scriptAssetAttachmentRepository; private ScriptAssetFileRepository scriptAssetFileRepository; + private MarketplaceScriptHeartRepository marketplaceScriptHeartRepository; private SettingsService settingsService; private MarketplaceScriptSeedLoader marketplaceScriptSeedLoader; @@ -74,6 +76,9 @@ class ChannelDirectoryServiceTest { scriptAssetRepository = mock(ScriptAssetRepository.class); scriptAssetAttachmentRepository = mock(ScriptAssetAttachmentRepository.class); scriptAssetFileRepository = mock(ScriptAssetFileRepository.class); + marketplaceScriptHeartRepository = mock(MarketplaceScriptHeartRepository.class); + when(marketplaceScriptHeartRepository.countByScriptIds(any())).thenReturn(List.of()); + when(marketplaceScriptHeartRepository.findByUsernameAndScriptIdIn(anyString(), any())).thenReturn(List.of()); settingsService = mock(SettingsService.class); when(settingsService.get()).thenReturn(Settings.defaults()); setupInMemoryPersistence(); @@ -109,6 +114,7 @@ class ChannelDirectoryServiceTest { scriptAssetRepository, scriptAssetAttachmentRepository, scriptAssetFileRepository, + marketplaceScriptHeartRepository, messagingTemplate, assetStorageService, mediaDetectionService, @@ -201,7 +207,7 @@ class ChannelDirectoryServiceTest { void includesDefaultMarketplaceScript() { when(scriptAssetRepository.findByIsPublicTrue()).thenReturn(List.of()); - List entries = service.listMarketplaceScripts(null); + List entries = service.listMarketplaceScripts(null, null); assertThat(entries) .anyMatch((entry) -> "rotating-logo".equals(entry.id()));