diff --git a/src/main/java/dev/kruhlmann/imgfloat/model/Asset.java b/src/main/java/dev/kruhlmann/imgfloat/model/Asset.java index 66c0556..696ffce 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/model/Asset.java +++ b/src/main/java/dev/kruhlmann/imgfloat/model/Asset.java @@ -32,6 +32,9 @@ public class Asset { @Column(name = "updated_at", nullable = false) private Instant updatedAt; + @Column(name = "display_order") + private Integer displayOrder; + public Asset() {} public Asset(String broadcaster, AssetType assetType) { @@ -57,6 +60,9 @@ public class Asset { if (this.assetType == null) { this.assetType = AssetType.OTHER; } + if (this.displayOrder != null && this.displayOrder < 1) { + this.displayOrder = 1; + } } public String getId() { @@ -95,6 +101,14 @@ public class Asset { this.updatedAt = updatedAt; } + public Integer getDisplayOrder() { + return displayOrder; + } + + public void setDisplayOrder(Integer displayOrder) { + this.displayOrder = displayOrder; + } + private static String normalize(String value) { return value == null ? null : value.toLowerCase(Locale.ROOT); } diff --git a/src/main/java/dev/kruhlmann/imgfloat/model/AssetPatch.java b/src/main/java/dev/kruhlmann/imgfloat/model/AssetPatch.java index bdffe30..f95814a 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/model/AssetPatch.java +++ b/src/main/java/dev/kruhlmann/imgfloat/model/AssetPatch.java @@ -16,7 +16,7 @@ public record AssetPatch( Double rotation, Double speed, Boolean muted, - Integer zIndex, + Integer order, Boolean hidden, Boolean audioLoop, Integer audioDelayMillis, @@ -37,7 +37,7 @@ public record AssetPatch( request.getRotation() != null ? changed(before.rotation(), asset.getRotation()) : null, request.getSpeed() != null ? changed(before.speed(), asset.getSpeed()) : null, request.getMuted() != null ? changed(before.muted(), asset.isMuted()) : null, - request.getZIndex() != null ? changed(before.zIndex(), asset.getZIndex()) : null, + request.getOrder() != null ? changed(before.order(), request.getOrder()) : null, null, null, null, @@ -112,7 +112,7 @@ public record AssetPatch( double rotation, double speed, boolean muted, - int zIndex, + int order, double audioVolume ) {} diff --git a/src/main/java/dev/kruhlmann/imgfloat/model/AssetView.java b/src/main/java/dev/kruhlmann/imgfloat/model/AssetView.java index 12a004a..6deab9c 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/model/AssetView.java +++ b/src/main/java/dev/kruhlmann/imgfloat/model/AssetView.java @@ -23,7 +23,6 @@ public record AssetView( String originalMediaType, AssetType assetType, List scriptAttachments, - Integer zIndex, Boolean audioLoop, Integer audioDelayMillis, Double audioSpeed, @@ -56,7 +55,6 @@ public record AssetView( visual.getOriginalMediaType(), asset.getAssetType(), null, - visual.getZIndex(), null, null, null, @@ -90,7 +88,6 @@ public record AssetView( audio.getOriginalMediaType(), asset.getAssetType(), null, - null, audio.isAudioLoop(), audio.getAudioDelayMillis(), audio.getAudioSpeed(), @@ -126,7 +123,6 @@ public record AssetView( script.getOriginalMediaType(), asset.getAssetType(), script.getAttachments(), - script.getZIndex(), null, null, null, diff --git a/src/main/java/dev/kruhlmann/imgfloat/model/ScriptAsset.java b/src/main/java/dev/kruhlmann/imgfloat/model/ScriptAsset.java index ed1ce01..7a5b399 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/model/ScriptAsset.java +++ b/src/main/java/dev/kruhlmann/imgfloat/model/ScriptAsset.java @@ -33,9 +33,6 @@ public class ScriptAsset { @Column(name = "source_file_id") private String sourceFileId; - @Column(name = "z_index") - private Integer zIndex; - @Transient private List attachments = List.of(); @@ -52,9 +49,6 @@ public class ScriptAsset { if (this.name == null || this.name.isBlank()) { this.name = this.id; } - if (this.zIndex == null || this.zIndex < 1) { - this.zIndex = 1; - } } public String getId() { @@ -121,14 +115,6 @@ public class ScriptAsset { this.sourceFileId = sourceFileId; } - public Integer getZIndex() { - return zIndex == null ? 1 : Math.max(1, zIndex); - } - - public void setZIndex(Integer zIndex) { - this.zIndex = zIndex == null ? null : Math.max(1, zIndex); - } - public List getAttachments() { return attachments == null ? List.of() : attachments; } diff --git a/src/main/java/dev/kruhlmann/imgfloat/model/TransformRequest.java b/src/main/java/dev/kruhlmann/imgfloat/model/TransformRequest.java index 4b99154..ac65122 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/model/TransformRequest.java +++ b/src/main/java/dev/kruhlmann/imgfloat/model/TransformRequest.java @@ -24,8 +24,8 @@ public class TransformRequest { private Boolean muted; - @Positive(message = "zIndex must be at least 1") - private Integer zIndex; + @Positive(message = "Order must be at least 1") + private Integer order; private Boolean audioLoop; @@ -100,12 +100,12 @@ public class TransformRequest { this.muted = muted; } - public Integer getZIndex() { - return zIndex; + public Integer getOrder() { + return order; } - public void setZIndex(Integer zIndex) { - this.zIndex = zIndex; + public void setOrder(Integer order) { + this.order = order; } public Boolean getAudioLoop() { diff --git a/src/main/java/dev/kruhlmann/imgfloat/model/VisualAsset.java b/src/main/java/dev/kruhlmann/imgfloat/model/VisualAsset.java index 0ec1d3e..74be278 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/model/VisualAsset.java +++ b/src/main/java/dev/kruhlmann/imgfloat/model/VisualAsset.java @@ -28,7 +28,6 @@ public class VisualAsset { private Boolean muted; private String mediaType; private String originalMediaType; - private Integer zIndex; private Double audioVolume; private boolean hidden; @@ -44,7 +43,6 @@ public class VisualAsset { this.rotation = 0; this.speed = 1.0; this.muted = false; - this.zIndex = 1; this.audioVolume = 1.0; this.hidden = true; } @@ -58,9 +56,6 @@ public class VisualAsset { if (this.muted == null) { this.muted = Boolean.FALSE; } - if (this.zIndex == null || this.zIndex < 1) { - this.zIndex = 1; - } if (this.audioVolume == null) { this.audioVolume = 1.0; } @@ -165,14 +160,6 @@ public class VisualAsset { this.originalMediaType = originalMediaType; } - public Integer getZIndex() { - return zIndex == null ? 1 : Math.max(1, zIndex); - } - - public void setZIndex(Integer zIndex) { - this.zIndex = zIndex == null ? null : Math.max(1, zIndex); - } - public double getAudioVolume() { return audioVolume == null ? 1.0 : Math.max(0.0, Math.min(1.0, audioVolume)); } diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java b/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java index 7926622..fc43dca 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java +++ b/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java @@ -189,10 +189,11 @@ public class ChannelDirectoryService { .stream() .map((visual) -> { Asset asset = assetById.get(visual.getId()); - return asset == null ? null : AssetView.fromVisual(normalized, asset, visual); + return asset == null ? null : Map.entry(asset, visual); }) .filter(Objects::nonNull) - .sorted(Comparator.comparingInt(AssetView::zIndex).reversed()) + .sorted(Comparator.comparingInt((Map.Entry entry) -> displayOrderValue(entry.getKey())).reversed()) + .map((entry) -> AssetView.fromVisual(normalized, entry.getKey(), entry.getValue())) .toList(); } @@ -318,6 +319,13 @@ public class ChannelDirectoryService { boolean isCode = isCodeMediaType(optimized.mediaType()) || isCodeMediaType(mediaType); AssetType assetType = AssetType.fromMediaType(optimized.mediaType(), mediaType); Asset asset = new Asset(channel.getBroadcaster(), assetType); + if (!isAudio && isCode) { + asset.setDisplayOrder(nextDisplayOrder(channel.getBroadcaster(), AssetType.SCRIPT)); + } else if (!isAudio) { + asset.setDisplayOrder( + nextDisplayOrder(channel.getBroadcaster(), AssetType.IMAGE, AssetType.VIDEO, AssetType.MODEL, AssetType.OTHER) + ); + } assetStorageService.storeAsset( channel.getBroadcaster(), @@ -340,7 +348,6 @@ public class ChannelDirectoryService { script.setMediaType(optimized.mediaType()); script.setOriginalMediaType(mediaType); script.setSourceFileId(asset.getId()); - script.setZIndex(nextScriptZIndex(channel.getBroadcaster())); script.setAttachments(List.of()); scriptAssetRepository.save(script); ScriptAssetFile sourceFile = new ScriptAssetFile(asset.getBroadcaster(), AssetType.SCRIPT); @@ -358,7 +365,6 @@ public class ChannelDirectoryService { visual.setOriginalMediaType(mediaType); visual.setMediaType(optimized.mediaType()); visual.setMuted(optimized.mediaType().startsWith("video/")); - visual.setZIndex(nextZIndex(channel.getBroadcaster())); assetStorageService.storePreview(channel.getBroadcaster(), asset.getId(), optimized.previewBytes()); visual.setPreview(optimized.previewBytes() != null ? asset.getId() + ".png" : ""); visualAssetRepository.save(visual); @@ -383,6 +389,7 @@ public class ChannelDirectoryService { enforceUploadLimit(bytes.length); Asset asset = new Asset(channel.getBroadcaster(), AssetType.SCRIPT); + asset.setDisplayOrder(nextDisplayOrder(channel.getBroadcaster(), AssetType.SCRIPT)); ScriptAssetFile sourceFile = new ScriptAssetFile(asset.getBroadcaster(), AssetType.SCRIPT); sourceFile.setId(asset.getId()); sourceFile.setMediaType(DEFAULT_CODE_MEDIA_TYPE); @@ -407,7 +414,6 @@ public class ChannelDirectoryService { script.setSourceFileId(sourceFile.getId()); script.setDescription(normalizeDescription(request.getDescription())); script.setPublic(Boolean.TRUE.equals(request.getIsPublic())); - script.setZIndex(nextScriptZIndex(channel.getBroadcaster())); script.setAttachments(List.of()); scriptAssetRepository.save(script); AssetView view = AssetView.fromScript(channel.getBroadcaster(), asset, script); @@ -787,6 +793,7 @@ public class ChannelDirectoryService { } Asset asset = new Asset(targetBroadcaster, AssetType.SCRIPT); + asset.setDisplayOrder(nextDisplayOrder(targetBroadcaster, AssetType.SCRIPT)); ScriptAssetFile sourceFile = new ScriptAssetFile(asset.getBroadcaster(), AssetType.SCRIPT); sourceFile.setId(asset.getId()); sourceFile.setMediaType(sourceContent.mediaType()); @@ -811,7 +818,6 @@ public class ChannelDirectoryService { script.setOriginalMediaType(sourceContent.mediaType()); script.setSourceFileId(sourceFile.getId()); script.setLogoFileId(sourceScript.getLogoFileId()); - script.setZIndex(nextScriptZIndex(targetBroadcaster)); script.setAttachments(List.of()); scriptAssetRepository.save(script); @@ -858,6 +864,7 @@ public class ChannelDirectoryService { } Asset asset = new Asset(targetBroadcaster, AssetType.SCRIPT); + asset.setDisplayOrder(nextDisplayOrder(targetBroadcaster, AssetType.SCRIPT)); ScriptAssetFile sourceFile = new ScriptAssetFile(asset.getBroadcaster(), AssetType.SCRIPT); sourceFile.setId(asset.getId()); sourceFile.setMediaType(sourceContent.mediaType()); @@ -881,7 +888,6 @@ public class ChannelDirectoryService { script.setMediaType(sourceContent.mediaType()); script.setOriginalMediaType(sourceContent.mediaType()); script.setSourceFileId(sourceFile.getId()); - script.setZIndex(nextScriptZIndex(targetBroadcaster)); script.setAttachments(List.of()); scriptAssetRepository.save(script); @@ -1009,14 +1015,19 @@ public class ChannelDirectoryService { ScriptAsset script = scriptAssetRepository .findById(asset.getId()) .orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not a script")); - int beforeZIndex = script.getZIndex(); - if (req.getZIndex() != null) { - if (req.getZIndex() < 1) { - throw new ResponseStatusException(BAD_REQUEST, "zIndex must be >= 1"); + Integer beforeOrder = asset.getDisplayOrder(); + List orderUpdates = List.of(); + if (req.getOrder() != null) { + if (req.getOrder() < 1) { + throw new ResponseStatusException(BAD_REQUEST, "Order must be >= 1"); } - script.setZIndex(req.getZIndex()); - scriptAssetRepository.save(script); - if (beforeZIndex != script.getZIndex()) { + orderUpdates = updateDisplayOrder( + normalized, + asset, + req.getOrder(), + EnumSet.of(AssetType.SCRIPT) + ); + if (!Objects.equals(beforeOrder, asset.getDisplayOrder())) { AssetPatch patch = new AssetPatch( asset.getId(), null, @@ -1026,7 +1037,7 @@ public class ChannelDirectoryService { null, null, null, - script.getZIndex(), + asset.getDisplayOrder(), null, null, null, @@ -1038,10 +1049,11 @@ public class ChannelDirectoryService { auditLogService.recordEntry( asset.getBroadcaster(), actor, - "SCRIPT_LAYER_UPDATED", - formatScriptTransformDetails(asset.getId(), script.getZIndex()) + "SCRIPT_ORDER_UPDATED", + formatScriptTransformDetails(asset.getId(), asset.getDisplayOrder()) ); } + publishOrderUpdates(broadcaster, asset.getId(), orderUpdates); } script.setAttachments(loadScriptAttachments(normalized, asset.getId(), null)); return AssetView.fromScript(normalized, asset, script); @@ -1058,17 +1070,28 @@ public class ChannelDirectoryService { visual.getRotation(), visual.getSpeed(), visual.isMuted(), - visual.getZIndex(), + displayOrderValue(asset), visual.getAudioVolume() ); validateVisualTransform(req); + List orderUpdates = List.of(); if (req.getX() != null) visual.setX(req.getX()); if (req.getY() != null) visual.setY(req.getY()); if (req.getWidth() != null) visual.setWidth(req.getWidth()); if (req.getHeight() != null) visual.setHeight(req.getHeight()); if (req.getRotation() != null) visual.setRotation(req.getRotation()); - if (req.getZIndex() != null) visual.setZIndex(req.getZIndex()); + if (req.getOrder() != null) { + orderUpdates = updateDisplayOrder( + normalized, + asset, + req.getOrder(), + EnumSet.of(AssetType.IMAGE, AssetType.VIDEO, AssetType.MODEL, AssetType.OTHER) + ); + if (asset.getDisplayOrder() != null) { + req.setOrder(asset.getDisplayOrder()); + } + } if (req.getSpeed() != null) visual.setSpeed(req.getSpeed()); if (req.getMuted() != null) visual.setMuted(req.getMuted()); if (req.getAudioVolume() != null) visual.setAudioVolume(req.getAudioVolume()); @@ -1086,6 +1109,7 @@ public class ChannelDirectoryService { formatVisualTransformDetails(asset.getId(), req) ); } + publishOrderUpdates(broadcaster, asset.getId(), orderUpdates); return view; }); } @@ -1113,9 +1137,9 @@ public class ChannelDirectoryService { if ( req.getSpeed() != null && (req.getSpeed() < minSpeed || req.getSpeed() > maxSpeed) ) throw new ResponseStatusException(BAD_REQUEST, "Speed out of range [" + minSpeed + " to " + maxSpeed + "]"); - if (req.getZIndex() != null && req.getZIndex() < 1) throw new ResponseStatusException( + if (req.getOrder() != null && req.getOrder() < 1) throw new ResponseStatusException( BAD_REQUEST, - "zIndex must be >= 1" + "Order must be >= 1" ); if ( req.getAudioVolume() != null && (req.getAudioVolume() < minVolume || req.getAudioVolume() > maxVolume) @@ -1580,51 +1604,100 @@ public class ChannelDirectoryService { return assets .stream() + .sorted( + Comparator.comparingInt((Asset asset) -> displayOrderValue(asset)) + .reversed() + .thenComparing(Asset::getCreatedAt, Comparator.nullsFirst(Comparator.naturalOrder())) + ) .map((asset) -> resolveAssetView(broadcaster, asset, visuals, audios, scripts, scriptAttachments)) .filter(Objects::nonNull) - .sorted( - Comparator.comparingInt((AssetView view) -> view.zIndex() == null ? Integer.MIN_VALUE : view.zIndex()) - .reversed() - .thenComparing(AssetView::createdAt, Comparator.nullsFirst(Comparator.naturalOrder())) - ) .toList(); } - private int nextZIndex(String broadcaster) { + private int nextDisplayOrder(String broadcaster, AssetType... types) { return ( - visualAssetRepository - .findByIdIn( - assetsWithType(normalize(broadcaster), AssetType.IMAGE, AssetType.VIDEO, AssetType.MODEL, AssetType.OTHER) - ) + assetRepository + .findByBroadcaster(normalize(broadcaster)) .stream() - .mapToInt(VisualAsset::getZIndex) + .filter((asset) -> Arrays.asList(types).contains(asset.getAssetType())) + .map(Asset::getDisplayOrder) + .filter(Objects::nonNull) + .mapToInt(Integer::intValue) .max() .orElse(0) + 1 ); } - private int nextScriptZIndex(String broadcaster) { - return ( - scriptAssetRepository - .findByIdIn(assetsWithType(normalize(broadcaster), AssetType.SCRIPT)) - .stream() - .mapToInt(ScriptAsset::getZIndex) - .max() - .orElse(0) + - 1 - ); + private int displayOrderValue(Asset asset) { + Integer value = asset == null ? null : asset.getDisplayOrder(); + return value == null ? Integer.MIN_VALUE : value; } - private List assetsWithType(String broadcaster, AssetType... types) { - Set typeSet = EnumSet.noneOf(AssetType.class); - typeSet.addAll(Arrays.asList(types)); - return assetRepository + private List updateDisplayOrder(String broadcaster, Asset target, int desiredOrder, Set types) { + if (target == null || types == null || types.isEmpty()) { + return List.of(); + } + List ordered = assetRepository .findByBroadcaster(normalize(broadcaster)) .stream() - .filter((asset) -> typeSet.contains(asset.getAssetType())) - .map(Asset::getId) - .toList(); + .filter((asset) -> types.contains(asset.getAssetType())) + .sorted( + Comparator.comparingInt((Asset asset) -> displayOrderValue(asset)) + .reversed() + .thenComparing(Asset::getCreatedAt, Comparator.nullsFirst(Comparator.naturalOrder())) + ) + .collect(Collectors.toCollection(ArrayList::new)); + + ordered.removeIf((asset) -> asset.getId().equals(target.getId())); + int insertIndex = Math.max(0, Math.min(ordered.size(), ordered.size() - desiredOrder)); + ordered.add(insertIndex, target); + + int size = ordered.size(); + List changed = new ArrayList<>(); + for (int index = 0; index < size; index++) { + Asset asset = ordered.get(index); + int nextOrder = size - index; + if (asset.getDisplayOrder() == null || asset.getDisplayOrder() != nextOrder) { + asset.setDisplayOrder(nextOrder); + changed.add(asset); + } + } + + if (!changed.isEmpty()) { + assetRepository.saveAll(changed); + } + return changed; + } + + private void publishOrderUpdates(String broadcaster, String targetAssetId, List updates) { + if (updates == null || updates.isEmpty()) { + return; + } + updates + .stream() + .filter((asset) -> asset.getDisplayOrder() != null) + .filter((asset) -> !asset.getId().equals(targetAssetId)) + .forEach((asset) -> { + AssetPatch patch = new AssetPatch( + asset.getId(), + null, + null, + null, + null, + null, + null, + null, + asset.getDisplayOrder(), + null, + null, + null, + null, + null, + null + ); + messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, patch)); + }); } private AssetView resolveAssetView(String broadcaster, Asset asset) { @@ -1818,7 +1891,7 @@ public class ChannelDirectoryService { if (req.getWidth() != null) parts.add("width=" + req.getWidth()); if (req.getHeight() != null) parts.add("height=" + req.getHeight()); if (req.getRotation() != null) parts.add("rotation=" + req.getRotation()); - if (req.getZIndex() != null) parts.add("zIndex=" + req.getZIndex()); + if (req.getOrder() != null) parts.add("order=" + req.getOrder()); if (req.getSpeed() != null) parts.add("speed=" + req.getSpeed()); if (req.getMuted() != null) parts.add("muted=" + req.getMuted()); if (req.getAudioVolume() != null) parts.add("audioVolume=" + req.getAudioVolume()); @@ -1835,10 +1908,10 @@ public class ChannelDirectoryService { return formatTransformDetails("Updated audio asset " + assetId, parts); } - private String formatScriptTransformDetails(String assetId, Integer zIndex) { + private String formatScriptTransformDetails(String assetId, Integer order) { String detail = "Updated script asset " + assetId; - if (zIndex != null) { - return detail + " (zIndex=" + zIndex + ")"; + if (order != null) { + return detail + " (order=" + order + ")"; } return detail; } @@ -1859,7 +1932,7 @@ public class ChannelDirectoryService { patch.rotation() != null || patch.speed() != null || patch.muted() != null || - patch.zIndex() != null || + patch.order() != null || patch.hidden() != null || patch.audioLoop() != null || patch.audioDelayMillis() != null || diff --git a/src/main/resources/db/migration/V8__asset_display_order.sql b/src/main/resources/db/migration/V8__asset_display_order.sql new file mode 100644 index 0000000..9edae7d --- /dev/null +++ b/src/main/resources/db/migration/V8__asset_display_order.sql @@ -0,0 +1,20 @@ +ALTER TABLE assets ADD COLUMN display_order INTEGER; + +UPDATE assets +SET display_order = visual_assets.z_index +FROM visual_assets +WHERE assets.id = visual_assets.id + AND assets.asset_type IN ('IMAGE', 'VIDEO', 'MODEL', 'OTHER'); + +UPDATE assets +SET display_order = script_assets.z_index +FROM script_assets +WHERE assets.id = script_assets.id + AND assets.asset_type = 'SCRIPT'; + +UPDATE assets +SET display_order = 1 +WHERE display_order IS NULL; + +ALTER TABLE visual_assets DROP COLUMN z_index; +ALTER TABLE script_assets DROP COLUMN z_index; diff --git a/src/main/resources/static/js/admin/console.js b/src/main/resources/static/js/admin/console.js index 89dd766..f486811 100644 --- a/src/main/resources/static/js/admin/console.js +++ b/src/main/resources/static/js/admin/console.js @@ -47,7 +47,7 @@ export function createAdminConsole({ const speedLabel = document.getElementById("asset-speed-label"); const volumeInput = document.getElementById("asset-volume"); const volumeLabel = document.getElementById("asset-volume-label"); - const selectedZLabel = document.getElementById("asset-z-level"); + const selectedOrderLabel = document.getElementById("asset-order-position"); const playbackSection = document.getElementById("playback-section"); const volumeSection = document.getElementById("volume-section"); const audioSection = document.getElementById("audio-section"); @@ -238,6 +238,20 @@ export function createAdminConsole({ return order.length - index; } + function getLayerPosition(assetId) { + const order = getLayerOrder(); + const index = order.indexOf(assetId); + if (index === -1) return 1; + return index + 1; + } + + function getScriptLayerPosition(assetId) { + const order = getScriptLayerOrder(); + const index = order.indexOf(assetId); + if (index === -1) return 1; + return index + 1; + } + function addPendingUpload(name) { const pending = { id: `pending-${Date.now()}-${Math.round(Math.random() * 100000)}`, @@ -662,23 +676,16 @@ export function createAdminConsole({ clearMedia(assetId); loopPlaybackState.delete(assetId); } - let targetLayer; - if (Number.isFinite(patch.layer)) { - targetLayer = patch.layer; - } else if (Number.isFinite(patch.zIndex)) { - targetLayer = patch.zIndex; - } else { - targetLayer = null; - } - if (!isAudio && Number.isFinite(targetLayer)) { + const targetOrder = Number.isFinite(patch.order) ? patch.order : null; + if (!isAudio && Number.isFinite(targetOrder)) { if (isScript) { const currentOrder = getScriptLayerOrder().filter((id) => id !== assetId); - const insertIndex = Math.max(0, currentOrder.length - Math.round(targetLayer)); + const insertIndex = Math.max(0, currentOrder.length - Math.round(targetOrder)); currentOrder.splice(insertIndex, 0, assetId); scriptLayerOrder = currentOrder; } else { const currentOrder = getLayerOrder().filter((id) => id !== assetId); - const insertIndex = Math.max(0, currentOrder.length - Math.round(targetLayer)); + const insertIndex = Math.max(0, currentOrder.length - Math.round(targetOrder)); currentOrder.splice(insertIndex, 0, assetId); layerOrder = currentOrder; } @@ -1340,9 +1347,9 @@ export function createAdminConsole({ return null; } if (isCodeAsset(asset)) { - return `Script layer ${getScriptLayerValue(asset.id)}`; + return `Script order ${getScriptLayerPosition(asset.id)} (above canvas assets)`; } - return `Layer ${getLayerValue(asset.id)}`; + return `Order ${getLayerPosition(asset.id)}`; } function createSectionHeader(title) { @@ -1392,11 +1399,16 @@ export function createAdminConsole({ } if (!isAudioAsset(asset)) { + const ordered = getLayeredAssets(asset); + const orderIndex = ordered.findIndex((item) => item.id === asset.id); + const canMoveUp = orderIndex > 0; + const canMoveDown = orderIndex !== -1 && orderIndex < ordered.length - 1; const moveUp = document.createElement("button"); moveUp.type = "button"; moveUp.className = "ghost icon-button"; moveUp.innerHTML = ''; - moveUp.title = isCodeAsset(asset) ? "Move script up" : "Move layer up"; + moveUp.title = isCodeAsset(asset) ? "Move script up" : "Move asset up"; + moveUp.disabled = !canMoveUp; moveUp.addEventListener("click", (e) => { e.stopPropagation(); moveLayerItem(asset, "up"); @@ -1405,7 +1417,8 @@ export function createAdminConsole({ moveDown.type = "button"; moveDown.className = "ghost icon-button"; moveDown.innerHTML = ''; - moveDown.title = isCodeAsset(asset) ? "Move script down" : "Move layer down"; + moveDown.title = isCodeAsset(asset) ? "Move script down" : "Move asset down"; + moveDown.disabled = !canMoveDown; moveDown.addEventListener("click", (e) => { e.stopPropagation(); moveLayerItem(asset, "down"); @@ -1802,8 +1815,10 @@ export function createAdminConsole({ controlsPanel.classList.remove("hidden"); lastSizeInputChanged = null; - if (selectedZLabel) { - selectedZLabel.textContent = getLayerValue(asset.id); + if (selectedOrderLabel) { + selectedOrderLabel.textContent = isCodeAsset(asset) + ? getScriptLayerPosition(asset.id) + : getLayerPosition(asset.id); } if (widthInput) widthInput.value = Math.round(asset.width); @@ -1921,9 +1936,12 @@ export function createAdminConsole({ if (asset) { selectedAssetBadges.appendChild(createBadge(getDisplayMediaType(asset))); if (isCodeAsset(asset)) { - selectedAssetBadges.appendChild(createBadge(`Script layer ${getScriptLayerValue(asset.id)}`, "subtle")); + selectedAssetBadges.appendChild( + createBadge(`Script order ${getScriptLayerPosition(asset.id)}`, "subtle"), + ); + selectedAssetBadges.appendChild(createBadge("Above canvas assets", "subtle")); } else if (!isAudioAsset(asset)) { - selectedAssetBadges.appendChild(createBadge(`Layer ${getLayerValue(asset.id)}`, "subtle")); + selectedAssetBadges.appendChild(createBadge(`Order ${getLayerPosition(asset.id)}`, "subtle")); } const aspectLabel = !isAudioAsset(asset) && !isCodeAsset(asset) ? formatAspectRatioLabel(asset) : ""; if (aspectLabel) { @@ -2445,18 +2463,17 @@ export function createAdminConsole({ audioVolume: asset.audioVolume, }; if (isCodeAsset(asset)) { - const layer = getScriptLayerValue(asset.id); - payload.zIndex = layer; + const order = getScriptLayerValue(asset.id); + payload.order = order; } else if (!isAudioAsset(asset)) { - const layer = getLayerValue(asset.id); + const order = getLayerValue(asset.id); payload.x = asset.x; payload.y = asset.y; payload.width = asset.width; payload.height = asset.height; payload.rotation = asset.rotation; payload.speed = asset.speed; - payload.layer = layer; - payload.zIndex = layer; + payload.order = order; if (isVideoAsset(asset)) { payload.muted = asset.muted; } diff --git a/src/main/resources/static/js/broadcast/renderer.js b/src/main/resources/static/js/broadcast/renderer.js index 5e1bfbc..baa6ede 100644 --- a/src/main/resources/static/js/broadcast/renderer.js +++ b/src/main/resources/static/js/broadcast/renderer.js @@ -291,21 +291,17 @@ export class BroadcastRenderer { this.hideAssetWithTransition(merged); return; } - const targetLayer = Number.isFinite(patch.layer) - ? patch.layer - : Number.isFinite(patch.zIndex) - ? patch.zIndex - : null; - if (Number.isFinite(targetLayer)) { + const targetOrder = Number.isFinite(patch.order) ? patch.order : null; + if (Number.isFinite(targetOrder)) { if (isScript) { const currentOrder = getScriptLayerOrder(this.state).filter((id) => id !== assetId); - const insertIndex = Math.max(0, currentOrder.length - Math.round(targetLayer)); + const insertIndex = Math.max(0, currentOrder.length - Math.round(targetOrder)); currentOrder.splice(insertIndex, 0, assetId); this.state.scriptLayerOrder = currentOrder; this.applyScriptCanvasOrder(); } else if (isVisual) { const currentOrder = getLayerOrder(this.state).filter((id) => id !== assetId); - const insertIndex = Math.max(0, currentOrder.length - Math.round(targetLayer)); + const insertIndex = Math.max(0, currentOrder.length - Math.round(targetOrder)); currentOrder.splice(insertIndex, 0, assetId); this.state.layerOrder = currentOrder; } @@ -827,12 +823,14 @@ export class BroadcastRenderer { return; } const ordered = getScriptLayerOrder(this.state); - ordered.forEach((id, index) => { + ordered + .slice() + .reverse() + .forEach((id) => { const canvas = this.scriptCanvases.get(id); if (!canvas) { return; } - canvas.style.zIndex = `${ordered.length - index}`; this.scriptLayer.appendChild(canvas); }); } diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html index 8e87152..b9ce71f 100644 --- a/src/main/resources/templates/admin.html +++ b/src/main/resources/templates/admin.html @@ -100,6 +100,10 @@
Layout & order
+

+ The top of the list renders on top in the broadcast. Script assets always render + above canvas assets. +

@@ -132,11 +136,11 @@
- Layer + Order
Layer 1Position 1
diff --git a/src/test/java/dev/kruhlmann/imgfloat/ChannelDirectoryServiceTest.java b/src/test/java/dev/kruhlmann/imgfloat/ChannelDirectoryServiceTest.java index 0a61d34..4adacb6 100644 --- a/src/test/java/dev/kruhlmann/imgfloat/ChannelDirectoryServiceTest.java +++ b/src/test/java/dev/kruhlmann/imgfloat/ChannelDirectoryServiceTest.java @@ -198,13 +198,13 @@ class ChannelDirectoryServiceTest { TransformRequest transform = validTransform(); transform.setSpeed(0.1); transform.setAudioVolume(0.01); - transform.setZIndex(1); + transform.setOrder(1); AssetView view = service.updateTransform(channel, id, transform, "caster").orElseThrow(); assertThat(view.speed()).isEqualTo(0.1); assertThat(view.audioVolume()).isEqualTo(0.01); - assertThat(view.zIndex()).isEqualTo(1); + assertThat(assetRepository.findById(id).orElseThrow().getDisplayOrder()).isEqualTo(1); } @Test