diff --git a/.gitattributes b/.gitattributes index 6a84827..55d82ca 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,3 +15,9 @@ doc/raw.png filter=lfs diff=lfs merge=lfs -text doc/raw.xcf filter=lfs diff=lfs merge=lfs -text dev/marketplace-scripts/rotating-logo/attachments/rotate.png filter=lfs diff=lfs merge=lfs -text dev/marketplace-scripts/rotating-logo/logo.png filter=lfs diff=lfs merge=lfs -text +doc/marketplace-scripts/checkerboard-test/logo.png filter=lfs diff=lfs merge=lfs -text +doc/marketplace-scripts/circle-grid/logo.png filter=lfs diff=lfs merge=lfs -text +doc/marketplace-scripts/color-bars/logo.png filter=lfs diff=lfs merge=lfs -text +doc/marketplace-scripts/mobius-strip/logo.png filter=lfs diff=lfs merge=lfs -text +doc/marketplace-scripts/rotating-logo/attachments/rotate.png filter=lfs diff=lfs merge=lfs -text +doc/marketplace-scripts/rotating-logo/logo.png filter=lfs diff=lfs merge=lfs -text diff --git a/dev/marketplace-scripts/rotating-logo/logo.png b/doc/marketplace-scripts/checkerboard-test/logo.png similarity index 100% rename from dev/marketplace-scripts/rotating-logo/logo.png rename to doc/marketplace-scripts/checkerboard-test/logo.png diff --git a/doc/marketplace-scripts/checkerboard-test/metadata.json b/doc/marketplace-scripts/checkerboard-test/metadata.json new file mode 100644 index 0000000..ec2c8a1 --- /dev/null +++ b/doc/marketplace-scripts/checkerboard-test/metadata.json @@ -0,0 +1,4 @@ +{ + "name": "Checkerboard test", + "description": "Animated checkerboard pattern with a moving offset to spot scaling artifacts." +} diff --git a/doc/marketplace-scripts/checkerboard-test/source.js b/doc/marketplace-scripts/checkerboard-test/source.js new file mode 100644 index 0000000..a1dfc64 --- /dev/null +++ b/doc/marketplace-scripts/checkerboard-test/source.js @@ -0,0 +1,30 @@ +function init() {} + +function tick(context, state) { + const { ctx, width, height, deltaMs } = context; + if (!ctx) { + return; + } + const speed = 0.02; + const offset = ((state.offset || 0) + (deltaMs || 0) * speed) % 40; + state.offset = offset; + + const squareSize = 40; + ctx.clearRect(0, 0, width, height); + for (let y = -squareSize; y < height + squareSize; y += squareSize) { + for (let x = -squareSize; x < width + squareSize; x += squareSize) { + const isDark = ((x + y) / squareSize) % 2 === 0; + ctx.fillStyle = isDark ? "#101828" : "#e2e8f0"; + ctx.fillRect(x + offset, y + offset, squareSize, squareSize); + } + } + + ctx.strokeStyle = "#ef4444"; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(width / 2, 0); + ctx.lineTo(width / 2, height); + ctx.moveTo(0, height / 2); + ctx.lineTo(width, height / 2); + ctx.stroke(); +} diff --git a/dev/marketplace-scripts/rotating-logo/attachments/rotate.png b/doc/marketplace-scripts/circle-grid/logo.png similarity index 100% rename from dev/marketplace-scripts/rotating-logo/attachments/rotate.png rename to doc/marketplace-scripts/circle-grid/logo.png diff --git a/doc/marketplace-scripts/circle-grid/metadata.json b/doc/marketplace-scripts/circle-grid/metadata.json new file mode 100644 index 0000000..e4b44a2 --- /dev/null +++ b/doc/marketplace-scripts/circle-grid/metadata.json @@ -0,0 +1,4 @@ +{ + "name": "Circle grid", + "description": "Concentric circles and calibration marks to test geometry and focus." +} diff --git a/doc/marketplace-scripts/circle-grid/source.js b/doc/marketplace-scripts/circle-grid/source.js new file mode 100644 index 0000000..4639afa --- /dev/null +++ b/doc/marketplace-scripts/circle-grid/source.js @@ -0,0 +1,42 @@ +function init() {} + +function tick(context, state) { + const { ctx, width, height, deltaMs } = context; + if (!ctx) { + return; + } + ctx.clearRect(0, 0, width, height); + + const centerX = width / 2; + const centerY = height / 2; + const maxRadius = Math.min(width, height) * 0.45; + const pulse = 0.02 * (deltaMs || 0); + state.phase = (state.phase || 0) + pulse; + + ctx.strokeStyle = "#38bdf8"; + ctx.lineWidth = 2; + for (let radius = maxRadius; radius > 0; radius -= maxRadius / 6) { + ctx.globalAlpha = 0.2 + (radius / maxRadius) * 0.6; + ctx.beginPath(); + ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); + ctx.stroke(); + } + ctx.globalAlpha = 1; + + ctx.strokeStyle = "#22c55e"; + ctx.lineWidth = 3; + const sweep = (Math.sin(state.phase) + 1) / 2; + const sweepRadius = maxRadius * (0.3 + sweep * 0.7); + ctx.beginPath(); + ctx.arc(centerX, centerY, sweepRadius, 0, Math.PI * 2); + ctx.stroke(); + + ctx.strokeStyle = "#f8fafc"; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(centerX, 0); + ctx.lineTo(centerX, height); + ctx.moveTo(0, centerY); + ctx.lineTo(width, centerY); + ctx.stroke(); +} diff --git a/doc/marketplace-scripts/color-bars/logo.png b/doc/marketplace-scripts/color-bars/logo.png new file mode 100644 index 0000000..bebb644 --- /dev/null +++ b/doc/marketplace-scripts/color-bars/logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ec08d7610f373892433ed67f85c3afb377aa08a097cec2b0b94656f00540f68 +size 1301 diff --git a/doc/marketplace-scripts/color-bars/metadata.json b/doc/marketplace-scripts/color-bars/metadata.json new file mode 100644 index 0000000..460e31b --- /dev/null +++ b/doc/marketplace-scripts/color-bars/metadata.json @@ -0,0 +1,4 @@ +{ + "name": "Color bars", + "description": "Classic SMPTE-style color bars test pattern for verifying color reproduction." +} diff --git a/doc/marketplace-scripts/color-bars/source.js b/doc/marketplace-scripts/color-bars/source.js new file mode 100644 index 0000000..c0e9eae --- /dev/null +++ b/doc/marketplace-scripts/color-bars/source.js @@ -0,0 +1,38 @@ +function init() {} + +function tick(context) { + const { ctx, width, height } = context; + if (!ctx) { + return; + } + ctx.clearRect(0, 0, width, height); + const topHeight = height * 0.7; + const barWidth = width / 7; + const colors = [ + "#ffffff", + "#ffff00", + "#00ffff", + "#00ff00", + "#ff00ff", + "#ff0000", + "#0000ff", + ]; + colors.forEach((color, index) => { + ctx.fillStyle = color; + ctx.fillRect(index * barWidth, 0, barWidth, topHeight); + }); + + const middleHeight = height * 0.15; + const middleColors = ["#0000ff", "#000000", "#ff00ff", "#000000", "#00ffff", "#000000", "#ffffff"]; + middleColors.forEach((color, index) => { + ctx.fillStyle = color; + ctx.fillRect(index * barWidth, topHeight, barWidth, middleHeight); + }); + + const bottomHeight = height - topHeight - middleHeight; + const bottomColors = ["#2b2b2b", "#ffffff", "#2b2b2b", "#00ffff", "#2b2b2b", "#ff00ff", "#2b2b2b"]; + bottomColors.forEach((color, index) => { + ctx.fillStyle = color; + ctx.fillRect(index * barWidth, topHeight + middleHeight, barWidth, bottomHeight); + }); +} diff --git a/doc/marketplace-scripts/mobius-strip/logo.png b/doc/marketplace-scripts/mobius-strip/logo.png new file mode 100644 index 0000000..bebb644 --- /dev/null +++ b/doc/marketplace-scripts/mobius-strip/logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ec08d7610f373892433ed67f85c3afb377aa08a097cec2b0b94656f00540f68 +size 1301 diff --git a/doc/marketplace-scripts/mobius-strip/metadata.json b/doc/marketplace-scripts/mobius-strip/metadata.json new file mode 100644 index 0000000..33faa19 --- /dev/null +++ b/doc/marketplace-scripts/mobius-strip/metadata.json @@ -0,0 +1,4 @@ +{ + "name": "Rotating Möbius strip", + "description": "Procedural Möbius strip rendering with rotation for geometry inspection." +} diff --git a/doc/marketplace-scripts/mobius-strip/source.js b/doc/marketplace-scripts/mobius-strip/source.js new file mode 100644 index 0000000..deaebaf --- /dev/null +++ b/doc/marketplace-scripts/mobius-strip/source.js @@ -0,0 +1,72 @@ +function init() {} + +function tick(context, state) { + const { ctx, width, height, deltaMs } = context; + if (!ctx) { + return; + } + const dt = (deltaMs || 16) * 0.001; + state.angle = (state.angle || 0) + dt * 0.6; + + ctx.clearRect(0, 0, width, height); + + const centerX = width / 2; + const centerY = height / 2; + const scale = Math.min(width, height) * 0.32; + + const points = []; + const segments = 140; + const halfWidth = 0.35; + + for (let i = 0; i <= segments; i += 1) { + const t = (i / segments) * Math.PI * 2; + const cosT = Math.cos(t); + const sinT = Math.sin(t); + + for (const side of [-halfWidth, halfWidth]) { + const cosHalf = Math.cos(t / 2); + const sinHalf = Math.sin(t / 2); + const radius = 1 + (side * cosHalf) / 2; + const x = radius * cosT; + const y = radius * sinT; + const z = (side * sinHalf) / 2; + + const y1 = y * Math.cos(state.angle) - z * Math.sin(state.angle); + const z1 = y * Math.sin(state.angle) + z * Math.cos(state.angle); + const x2 = x * Math.cos(state.angle * 0.7) - z1 * Math.sin(state.angle * 0.7); + const z2 = x * Math.sin(state.angle * 0.7) + z1 * Math.cos(state.angle * 0.7); + + const depth = 2.6; + const perspective = depth / (depth - z2); + const screenX = centerX + x2 * scale * perspective; + const screenY = centerY + y1 * scale * perspective; + points.push({ screenX, screenY, depth: z2 }); + } + } + + ctx.lineWidth = 2; + for (let i = 0; i < points.length - 2; i += 2) { + const a = points[i]; + const b = points[i + 1]; + const brightness = Math.max(0.25, (a.depth + 1.5) / 3); + ctx.strokeStyle = `rgba(56, 189, 248, ${brightness})`; + ctx.beginPath(); + ctx.moveTo(a.screenX, a.screenY); + ctx.lineTo(b.screenX, b.screenY); + ctx.stroke(); + } + + ctx.strokeStyle = "rgba(248, 250, 252, 0.7)"; + ctx.lineWidth = 1; + ctx.beginPath(); + for (let i = 0; i < points.length; i += 2) { + const p = points[i]; + if (i === 0) { + ctx.moveTo(p.screenX, p.screenY); + } else { + ctx.lineTo(p.screenX, p.screenY); + } + } + ctx.closePath(); + ctx.stroke(); +} diff --git a/doc/marketplace-scripts/rotating-logo/attachments/rotate.png b/doc/marketplace-scripts/rotating-logo/attachments/rotate.png new file mode 100644 index 0000000..bebb644 --- /dev/null +++ b/doc/marketplace-scripts/rotating-logo/attachments/rotate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ec08d7610f373892433ed67f85c3afb377aa08a097cec2b0b94656f00540f68 +size 1301 diff --git a/doc/marketplace-scripts/rotating-logo/logo.png b/doc/marketplace-scripts/rotating-logo/logo.png new file mode 100644 index 0000000..bebb644 --- /dev/null +++ b/doc/marketplace-scripts/rotating-logo/logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ec08d7610f373892433ed67f85c3afb377aa08a097cec2b0b94656f00540f68 +size 1301 diff --git a/dev/marketplace-scripts/rotating-logo/metadata.json b/doc/marketplace-scripts/rotating-logo/metadata.json similarity index 100% rename from dev/marketplace-scripts/rotating-logo/metadata.json rename to doc/marketplace-scripts/rotating-logo/metadata.json diff --git a/dev/marketplace-scripts/rotating-logo/source.js b/doc/marketplace-scripts/rotating-logo/source.js similarity index 100% rename from dev/marketplace-scripts/rotating-logo/source.js rename to doc/marketplace-scripts/rotating-logo/source.js diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java b/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java index ba40003..ffd4c52 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java +++ b/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java @@ -643,6 +643,7 @@ public class ChannelDirectoryService { } script.setAttachments(loadScriptAttachments(asset.getBroadcaster(), asset.getId(), null)); + scriptAssetRepository.save(script); AssetView view = AssetView.fromScript(asset.getBroadcaster(), asset, script); messagingTemplate.convertAndSend(topicFor(targetBroadcaster), AssetEvent.created(targetBroadcaster, view)); return Optional.of(view); diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/MarketplaceScriptSeedLoader.java b/src/main/java/dev/kruhlmann/imgfloat/service/MarketplaceScriptSeedLoader.java index 745720b..e0522e0 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/service/MarketplaceScriptSeedLoader.java +++ b/src/main/java/dev/kruhlmann/imgfloat/service/MarketplaceScriptSeedLoader.java @@ -196,9 +196,9 @@ public class MarketplaceScriptSeedLoader { if (rootPath != null && !rootPath.isBlank()) { return Path.of(rootPath); } - Path devPath = Path.of("dev", "marketplace-scripts"); - if (Files.isDirectory(devPath)) { - return devPath; + Path docsPath = Path.of("doc", "marketplace-scripts"); + if (Files.isDirectory(docsPath)) { + return docsPath; } return null; } diff --git a/src/main/resources/static/css/customAssets.css b/src/main/resources/static/css/customAssets.css index af9dcd6..8989b4d 100644 --- a/src/main/resources/static/css/customAssets.css +++ b/src/main/resources/static/css/customAssets.css @@ -236,7 +236,8 @@ .modal .modal-inner .marketplace-list { display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + grid-template-columns: repeat(auto-fit, 240px); + grid-auto-rows: 240px; gap: 16px; margin-top: 12px; } @@ -250,7 +251,8 @@ border: 1px solid rgba(148, 163, 184, 0.3); border-radius: 10px; background-color: rgba(15, 23, 42, 0.6); - min-height: 240px; + width: 240px; + height: 240px; } .modal .modal-inner .marketplace-logo { @@ -273,11 +275,31 @@ display: flex; flex-direction: column; gap: 6px; + width: 100%; +} + +.modal .modal-inner .marketplace-content strong { + display: block; + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .modal .modal-inner .marketplace-content p { margin: 0; color: rgba(226, 232, 240, 0.8); + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.modal .modal-inner .marketplace-content small { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .modal .modal-inner .marketplace-actions { diff --git a/src/main/resources/static/js/admin/console.js b/src/main/resources/static/js/admin/console.js index fa40d7e..20b5eea 100644 --- a/src/main/resources/static/js/admin/console.js +++ b/src/main/resources/static/js/admin/console.js @@ -1482,6 +1482,14 @@ export function createAdminConsole({ function createPreviewElement(asset) { if (isCodeAsset(asset)) { + if (asset.logoUrl) { + const img = document.createElement("img"); + img.className = "asset-preview"; + img.src = asset.logoUrl; + img.alt = asset.name || "Script logo"; + img.loading = "lazy"; + return img; + } const icon = document.createElement("div"); icon.className = "asset-preview code-icon"; icon.innerHTML = '';