mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Fix layering and add release ci
This commit is contained in:
53
.github/workflows/ci.yml
vendored
53
.github/workflows/ci.yml
vendored
@@ -39,14 +39,46 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: build
|
||||||
|
outputs:
|
||||||
|
pom_version: ${{ steps.read_version.outputs.pom_version }}
|
||||||
|
version_changed: ${{ steps.version_check.outputs.version_changed }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
|
|
||||||
|
- name: Check for pom.xml changes
|
||||||
|
id: version_check
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" != "push" ]; then
|
||||||
|
echo "version_changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- pom.xml | grep -q '^pom.xml$'; then
|
||||||
|
echo "version_changed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "version_changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Read pom.xml version
|
||||||
|
id: read_version
|
||||||
|
run: |
|
||||||
|
VERSION=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version)
|
||||||
|
echo "pom_version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Set Docker tags
|
||||||
|
id: docker_tags
|
||||||
|
run: |
|
||||||
|
TAGS="kruhlmann/imgfloat-j:latest,kruhlmann/imgfloat-j:${{ github.sha }}"
|
||||||
|
if [ "${{ steps.version_check.outputs.version_changed }}" = "true" ]; then
|
||||||
|
TAGS="$TAGS,kruhlmann/imgfloat-j:${{ steps.read_version.outputs.pom_version }}"
|
||||||
|
fi
|
||||||
|
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -59,6 +91,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||||
tags: |
|
tags: ${{ steps.docker_tags.outputs.tags }}
|
||||||
kruhlmann/imgfloat-j:latest
|
|
||||||
kruhlmann/imgfloat-j:${{ github.sha }}
|
release:
|
||||||
|
timeout-minutes: 10
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: docker
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && needs.docker.outputs.version_changed == 'true'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
lfs: false
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: v${{ needs.docker.outputs.pom_version }}
|
||||||
|
name: v${{ needs.docker.outputs.pom_version }}
|
||||||
|
generate_release_notes: true
|
||||||
|
|||||||
2
pom.xml
2
pom.xml
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<groupId>dev.kruhlmann</groupId>
|
<groupId>dev.kruhlmann</groupId>
|
||||||
<artifactId>imgfloat</artifactId>
|
<artifactId>imgfloat</artifactId>
|
||||||
<version>0.0.1</version>
|
<version>1.0.0</version>
|
||||||
<name>Imgfloat</name>
|
<name>Imgfloat</name>
|
||||||
<description>Livestream overlay with Twitch-authenticated channel admins and broadcasters.</description>
|
<description>Livestream overlay with Twitch-authenticated channel admins and broadcasters.</description>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ public record AssetView(
|
|||||||
script.getOriginalMediaType(),
|
script.getOriginalMediaType(),
|
||||||
asset.getAssetType(),
|
asset.getAssetType(),
|
||||||
script.getAttachments(),
|
script.getAttachments(),
|
||||||
null,
|
script.getZIndex(),
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ public class ScriptAsset {
|
|||||||
@Column(name = "source_file_id")
|
@Column(name = "source_file_id")
|
||||||
private String sourceFileId;
|
private String sourceFileId;
|
||||||
|
|
||||||
|
@Column(name = "z_index")
|
||||||
|
private Integer zIndex;
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
private List<ScriptAssetAttachmentView> attachments = List.of();
|
private List<ScriptAssetAttachmentView> attachments = List.of();
|
||||||
|
|
||||||
@@ -49,6 +52,9 @@ public class ScriptAsset {
|
|||||||
if (this.name == null || this.name.isBlank()) {
|
if (this.name == null || this.name.isBlank()) {
|
||||||
this.name = this.id;
|
this.name = this.id;
|
||||||
}
|
}
|
||||||
|
if (this.zIndex == null || this.zIndex < 1) {
|
||||||
|
this.zIndex = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
@@ -115,6 +121,14 @@ public class ScriptAsset {
|
|||||||
this.sourceFileId = sourceFileId;
|
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<ScriptAssetAttachmentView> getAttachments() {
|
public List<ScriptAssetAttachmentView> getAttachments() {
|
||||||
return attachments == null ? List.of() : attachments;
|
return attachments == null ? List.of() : attachments;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ public class ChannelDirectoryService {
|
|||||||
return asset == null ? null : AssetView.fromVisual(normalized, asset, visual);
|
return asset == null ? null : AssetView.fromVisual(normalized, asset, visual);
|
||||||
})
|
})
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.sorted(Comparator.comparingInt(AssetView::zIndex))
|
.sorted(Comparator.comparingInt(AssetView::zIndex).reversed())
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,6 +273,7 @@ public class ChannelDirectoryService {
|
|||||||
script.setMediaType(optimized.mediaType());
|
script.setMediaType(optimized.mediaType());
|
||||||
script.setOriginalMediaType(mediaType);
|
script.setOriginalMediaType(mediaType);
|
||||||
script.setSourceFileId(asset.getId());
|
script.setSourceFileId(asset.getId());
|
||||||
|
script.setZIndex(nextScriptZIndex(channel.getBroadcaster()));
|
||||||
script.setAttachments(List.of());
|
script.setAttachments(List.of());
|
||||||
scriptAssetRepository.save(script);
|
scriptAssetRepository.save(script);
|
||||||
ScriptAssetFile sourceFile = new ScriptAssetFile(asset.getBroadcaster(), AssetType.SCRIPT);
|
ScriptAssetFile sourceFile = new ScriptAssetFile(asset.getBroadcaster(), AssetType.SCRIPT);
|
||||||
@@ -333,6 +334,7 @@ public class ChannelDirectoryService {
|
|||||||
script.setSourceFileId(sourceFile.getId());
|
script.setSourceFileId(sourceFile.getId());
|
||||||
script.setDescription(normalizeDescription(request.getDescription()));
|
script.setDescription(normalizeDescription(request.getDescription()));
|
||||||
script.setPublic(Boolean.TRUE.equals(request.getIsPublic()));
|
script.setPublic(Boolean.TRUE.equals(request.getIsPublic()));
|
||||||
|
script.setZIndex(nextScriptZIndex(channel.getBroadcaster()));
|
||||||
script.setAttachments(List.of());
|
script.setAttachments(List.of());
|
||||||
scriptAssetRepository.save(script);
|
scriptAssetRepository.save(script);
|
||||||
AssetView view = AssetView.fromScript(channel.getBroadcaster(), asset, script);
|
AssetView view = AssetView.fromScript(channel.getBroadcaster(), asset, script);
|
||||||
@@ -711,6 +713,7 @@ public class ChannelDirectoryService {
|
|||||||
script.setOriginalMediaType(sourceContent.mediaType());
|
script.setOriginalMediaType(sourceContent.mediaType());
|
||||||
script.setSourceFileId(sourceFile.getId());
|
script.setSourceFileId(sourceFile.getId());
|
||||||
script.setLogoFileId(sourceScript.getLogoFileId());
|
script.setLogoFileId(sourceScript.getLogoFileId());
|
||||||
|
script.setZIndex(nextScriptZIndex(targetBroadcaster));
|
||||||
script.setAttachments(List.of());
|
script.setAttachments(List.of());
|
||||||
scriptAssetRepository.save(script);
|
scriptAssetRepository.save(script);
|
||||||
|
|
||||||
@@ -770,6 +773,7 @@ public class ChannelDirectoryService {
|
|||||||
script.setMediaType(sourceContent.mediaType());
|
script.setMediaType(sourceContent.mediaType());
|
||||||
script.setOriginalMediaType(sourceContent.mediaType());
|
script.setOriginalMediaType(sourceContent.mediaType());
|
||||||
script.setSourceFileId(sourceFile.getId());
|
script.setSourceFileId(sourceFile.getId());
|
||||||
|
script.setZIndex(nextScriptZIndex(targetBroadcaster));
|
||||||
script.setAttachments(List.of());
|
script.setAttachments(List.of());
|
||||||
scriptAssetRepository.save(script);
|
scriptAssetRepository.save(script);
|
||||||
|
|
||||||
@@ -891,6 +895,34 @@ public class ChannelDirectoryService {
|
|||||||
ScriptAsset script = scriptAssetRepository
|
ScriptAsset script = scriptAssetRepository
|
||||||
.findById(asset.getId())
|
.findById(asset.getId())
|
||||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not a script"));
|
.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");
|
||||||
|
}
|
||||||
|
script.setZIndex(req.getZIndex());
|
||||||
|
scriptAssetRepository.save(script);
|
||||||
|
if (beforeZIndex != script.getZIndex()) {
|
||||||
|
AssetPatch patch = new AssetPatch(
|
||||||
|
asset.getId(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
script.getZIndex(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, patch));
|
||||||
|
}
|
||||||
|
}
|
||||||
script.setAttachments(loadScriptAttachments(normalized, asset.getId(), null));
|
script.setAttachments(loadScriptAttachments(normalized, asset.getId(), null));
|
||||||
return AssetView.fromScript(normalized, asset, script);
|
return AssetView.fromScript(normalized, asset, script);
|
||||||
}
|
}
|
||||||
@@ -1377,9 +1409,9 @@ public class ChannelDirectoryService {
|
|||||||
.map((asset) -> resolveAssetView(broadcaster, asset, visuals, audios, scripts, scriptAttachments))
|
.map((asset) -> resolveAssetView(broadcaster, asset, visuals, audios, scripts, scriptAttachments))
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.sorted(
|
.sorted(
|
||||||
Comparator.comparing((AssetView view) ->
|
Comparator.comparingInt((AssetView view) -> view.zIndex() == null ? Integer.MIN_VALUE : view.zIndex())
|
||||||
view.zIndex() == null ? Integer.MAX_VALUE : view.zIndex()
|
.reversed()
|
||||||
).thenComparing(AssetView::createdAt, Comparator.nullsFirst(Comparator.naturalOrder()))
|
.thenComparing(AssetView::createdAt, Comparator.nullsFirst(Comparator.naturalOrder()))
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
@@ -1398,6 +1430,18 @@ public class ChannelDirectoryService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int nextScriptZIndex(String broadcaster) {
|
||||||
|
return (
|
||||||
|
scriptAssetRepository
|
||||||
|
.findByIdIn(assetsWithType(normalize(broadcaster), AssetType.SCRIPT))
|
||||||
|
.stream()
|
||||||
|
.mapToInt(ScriptAsset::getZIndex)
|
||||||
|
.max()
|
||||||
|
.orElse(0) +
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private List<String> assetsWithType(String broadcaster, AssetType... types) {
|
private List<String> assetsWithType(String broadcaster, AssetType... types) {
|
||||||
Set<AssetType> typeSet = EnumSet.noneOf(AssetType.class);
|
Set<AssetType> typeSet = EnumSet.noneOf(AssetType.class);
|
||||||
typeSet.addAll(Arrays.asList(types));
|
typeSet.addAll(Arrays.asList(types));
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE script_assets ADD COLUMN IF NOT EXISTS z_index INTEGER NOT NULL DEFAULT 1;
|
||||||
|
|
||||||
|
UPDATE script_assets
|
||||||
|
SET z_index = 1
|
||||||
|
WHERE z_index IS NULL;
|
||||||
@@ -1740,10 +1740,19 @@ button:disabled:hover {
|
|||||||
.asset-list {
|
.asset-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.asset-section {
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(148, 163, 184, 0.9);
|
||||||
|
padding: 4px 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.asset-item {
|
.asset-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ensureLayerPosition as ensureLayerPositionForState,
|
ensureLayerPosition as ensureLayerPositionForState,
|
||||||
getLayerOrder as getLayerOrderForState,
|
getLayerOrder as getLayerOrderForState,
|
||||||
getRenderOrder as getRenderOrderForState,
|
getRenderOrder as getRenderOrderForState,
|
||||||
|
getScriptLayerOrder as getScriptLayerOrderForState,
|
||||||
} from "../broadcast/layers.js";
|
} from "../broadcast/layers.js";
|
||||||
|
|
||||||
export function createAdminConsole({
|
export function createAdminConsole({
|
||||||
@@ -80,6 +81,7 @@ export function createAdminConsole({
|
|||||||
|
|
||||||
let drawPending = false;
|
let drawPending = false;
|
||||||
let layerOrder = [];
|
let layerOrder = [];
|
||||||
|
let scriptLayerOrder = [];
|
||||||
const layerState = {
|
const layerState = {
|
||||||
assets,
|
assets,
|
||||||
get layerOrder() {
|
get layerOrder() {
|
||||||
@@ -88,6 +90,12 @@ export function createAdminConsole({
|
|||||||
set layerOrder(value) {
|
set layerOrder(value) {
|
||||||
layerOrder = value;
|
layerOrder = value;
|
||||||
},
|
},
|
||||||
|
get scriptLayerOrder() {
|
||||||
|
return scriptLayerOrder;
|
||||||
|
},
|
||||||
|
set scriptLayerOrder(value) {
|
||||||
|
scriptLayerOrder = value;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let pendingUploads = [];
|
let pendingUploads = [];
|
||||||
let selectedAssetId = null;
|
let selectedAssetId = null;
|
||||||
@@ -178,12 +186,22 @@ export function createAdminConsole({
|
|||||||
return getLayerOrderForState(layerState);
|
return getLayerOrderForState(layerState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getScriptLayerOrder() {
|
||||||
|
return getScriptLayerOrderForState(layerState);
|
||||||
|
}
|
||||||
|
|
||||||
function getAssetsByLayer() {
|
function getAssetsByLayer() {
|
||||||
return getLayerOrder()
|
return getLayerOrder()
|
||||||
.map((id) => assets.get(id))
|
.map((id) => assets.get(id))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getScriptAssetsByLayer() {
|
||||||
|
return getScriptLayerOrder()
|
||||||
|
.map((id) => assets.get(id))
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
function getAudioAssets() {
|
function getAudioAssets() {
|
||||||
return Array.from(assets.values())
|
return Array.from(assets.values())
|
||||||
.filter((asset) => isAudioAsset(asset))
|
.filter((asset) => isAudioAsset(asset))
|
||||||
@@ -191,9 +209,7 @@ export function createAdminConsole({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCodeAssets() {
|
function getCodeAssets() {
|
||||||
return Array.from(assets.values())
|
return getScriptAssetsByLayer();
|
||||||
.filter((asset) => isCodeAsset(asset))
|
|
||||||
.sort((a, b) => (b.createdAtMs || 0) - (a.createdAtMs || 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRenderOrder() {
|
function getRenderOrder() {
|
||||||
@@ -211,6 +227,17 @@ export function createAdminConsole({
|
|||||||
return order.length - index;
|
return order.length - index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getScriptLayerValue(assetId) {
|
||||||
|
const asset = assets.get(assetId);
|
||||||
|
if (!asset || !isCodeAsset(asset)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const order = getScriptLayerOrder();
|
||||||
|
const index = order.indexOf(assetId);
|
||||||
|
if (index === -1) return 1;
|
||||||
|
return order.length - index;
|
||||||
|
}
|
||||||
|
|
||||||
function addPendingUpload(name) {
|
function addPendingUpload(name) {
|
||||||
const pending = {
|
const pending = {
|
||||||
id: `pending-${Date.now()}-${Math.round(Math.random() * 100000)}`,
|
id: `pending-${Date.now()}-${Math.round(Math.random() * 100000)}`,
|
||||||
@@ -547,6 +574,7 @@ export function createAdminConsole({
|
|||||||
|
|
||||||
function renderAssets(list) {
|
function renderAssets(list) {
|
||||||
layerOrder = [];
|
layerOrder = [];
|
||||||
|
scriptLayerOrder = [];
|
||||||
list.forEach((item) => storeAsset(item, { placement: "append" }));
|
list.forEach((item) => storeAsset(item, { placement: "append" }));
|
||||||
drawAndList();
|
drawAndList();
|
||||||
}
|
}
|
||||||
@@ -594,6 +622,7 @@ export function createAdminConsole({
|
|||||||
if (event.type === "DELETED") {
|
if (event.type === "DELETED") {
|
||||||
assets.delete(assetId);
|
assets.delete(assetId);
|
||||||
layerOrder = layerOrder.filter((id) => id !== assetId);
|
layerOrder = layerOrder.filter((id) => id !== assetId);
|
||||||
|
scriptLayerOrder = scriptLayerOrder.filter((id) => id !== assetId);
|
||||||
clearMedia(assetId);
|
clearMedia(assetId);
|
||||||
renderStates.delete(assetId);
|
renderStates.delete(assetId);
|
||||||
loopPlaybackState.delete(assetId);
|
loopPlaybackState.delete(assetId);
|
||||||
@@ -628,6 +657,7 @@ export function createAdminConsole({
|
|||||||
}
|
}
|
||||||
const merged = { ...existing, ...patch };
|
const merged = { ...existing, ...patch };
|
||||||
const isAudio = isAudioAsset(merged);
|
const isAudio = isAudioAsset(merged);
|
||||||
|
const isScript = isCodeAsset(merged);
|
||||||
if (patch.hidden) {
|
if (patch.hidden) {
|
||||||
clearMedia(assetId);
|
clearMedia(assetId);
|
||||||
loopPlaybackState.delete(assetId);
|
loopPlaybackState.delete(assetId);
|
||||||
@@ -641,10 +671,17 @@ export function createAdminConsole({
|
|||||||
targetLayer = null;
|
targetLayer = null;
|
||||||
}
|
}
|
||||||
if (!isAudio && Number.isFinite(targetLayer)) {
|
if (!isAudio && Number.isFinite(targetLayer)) {
|
||||||
const currentOrder = getLayerOrder().filter((id) => id !== assetId);
|
if (isScript) {
|
||||||
const insertIndex = Math.max(0, currentOrder.length - Math.round(targetLayer));
|
const currentOrder = getScriptLayerOrder().filter((id) => id !== assetId);
|
||||||
currentOrder.splice(insertIndex, 0, assetId);
|
const insertIndex = Math.max(0, currentOrder.length - Math.round(targetLayer));
|
||||||
layerOrder = currentOrder;
|
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));
|
||||||
|
currentOrder.splice(insertIndex, 0, assetId);
|
||||||
|
layerOrder = currentOrder;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
storeAsset(merged);
|
storeAsset(merged);
|
||||||
if (!isAudio) {
|
if (!isAudio) {
|
||||||
@@ -1298,6 +1335,138 @@ export function createAdminConsole({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLayerDetail(asset) {
|
||||||
|
if (!asset || isAudioAsset(asset)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (isCodeAsset(asset)) {
|
||||||
|
return `Script layer ${getScriptLayerValue(asset.id)}`;
|
||||||
|
}
|
||||||
|
return `Layer ${getLayerValue(asset.id)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSectionHeader(title) {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.className = "asset-section";
|
||||||
|
li.textContent = title;
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendAssetListItem(list, asset) {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.className = "asset-item";
|
||||||
|
if (asset.id === selectedAssetId) {
|
||||||
|
li.classList.add("selected");
|
||||||
|
}
|
||||||
|
li.classList.toggle("is-hidden", !!asset.hidden);
|
||||||
|
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "asset-row";
|
||||||
|
|
||||||
|
const preview = createPreviewElement(asset);
|
||||||
|
|
||||||
|
const meta = document.createElement("div");
|
||||||
|
meta.className = "meta";
|
||||||
|
const name = document.createElement("strong");
|
||||||
|
name.textContent = asset.name || `Asset ${asset.id.slice(0, 6)}`;
|
||||||
|
const details = document.createElement("small");
|
||||||
|
const layerDetail = getLayerDetail(asset);
|
||||||
|
details.textContent = layerDetail ? `${getAssetTypeLabel(asset)} · ${layerDetail}` : getAssetTypeLabel(asset);
|
||||||
|
meta.appendChild(name);
|
||||||
|
meta.appendChild(details);
|
||||||
|
|
||||||
|
const actions = document.createElement("div");
|
||||||
|
actions.className = "actions";
|
||||||
|
|
||||||
|
if (isCodeAsset(asset)) {
|
||||||
|
const editBtn = document.createElement("button");
|
||||||
|
editBtn.type = "button";
|
||||||
|
editBtn.className = "ghost icon-button";
|
||||||
|
editBtn.innerHTML = '<i class="fa-solid fa-code"></i>';
|
||||||
|
editBtn.title = "Edit script";
|
||||||
|
editBtn.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
customAssetModal?.openEditor?.(asset);
|
||||||
|
});
|
||||||
|
actions.appendChild(editBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAudioAsset(asset)) {
|
||||||
|
const moveUp = document.createElement("button");
|
||||||
|
moveUp.type = "button";
|
||||||
|
moveUp.className = "ghost icon-button";
|
||||||
|
moveUp.innerHTML = '<i class="fa-solid fa-arrow-up"></i>';
|
||||||
|
moveUp.title = isCodeAsset(asset) ? "Move script up" : "Move layer up";
|
||||||
|
moveUp.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
moveLayerItem(asset, "up");
|
||||||
|
});
|
||||||
|
const moveDown = document.createElement("button");
|
||||||
|
moveDown.type = "button";
|
||||||
|
moveDown.className = "ghost icon-button";
|
||||||
|
moveDown.innerHTML = '<i class="fa-solid fa-arrow-down"></i>';
|
||||||
|
moveDown.title = isCodeAsset(asset) ? "Move script down" : "Move layer down";
|
||||||
|
moveDown.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
moveLayerItem(asset, "down");
|
||||||
|
});
|
||||||
|
actions.appendChild(moveUp);
|
||||||
|
actions.appendChild(moveDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAudioAsset(asset)) {
|
||||||
|
const playBtn = document.createElement("button");
|
||||||
|
playBtn.type = "button";
|
||||||
|
playBtn.className = "ghost icon-button";
|
||||||
|
const isLooping = !!asset.audioLoop;
|
||||||
|
const isPlayingLoop = getLoopPlaybackState(asset);
|
||||||
|
updatePlayButtonIcon(playBtn, isLooping, isPlayingLoop);
|
||||||
|
playBtn.title = isLooping
|
||||||
|
? isPlayingLoop
|
||||||
|
? "Pause looping audio"
|
||||||
|
: "Play looping audio"
|
||||||
|
: "Play audio";
|
||||||
|
playBtn.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const nextPlay = isLooping ? !(loopPlaybackState.get(asset.id) ?? getLoopPlaybackState(asset)) : true;
|
||||||
|
if (isLooping) {
|
||||||
|
loopPlaybackState.set(asset.id, nextPlay);
|
||||||
|
updatePlayButtonIcon(playBtn, true, nextPlay);
|
||||||
|
playBtn.title = nextPlay ? "Pause looping audio" : "Play looping audio";
|
||||||
|
}
|
||||||
|
triggerAudioPlayback(asset, nextPlay);
|
||||||
|
});
|
||||||
|
actions.appendChild(playBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAudioAsset(asset) && !isCodeAsset(asset)) {
|
||||||
|
const toggleBtn = document.createElement("button");
|
||||||
|
toggleBtn.type = "button";
|
||||||
|
toggleBtn.className = "ghost icon-button";
|
||||||
|
toggleBtn.innerHTML = `<i class="fa-solid ${asset.hidden ? "fa-eye" : "fa-eye-slash"}"></i>`;
|
||||||
|
toggleBtn.title = asset.hidden ? "Show asset" : "Hide asset";
|
||||||
|
toggleBtn.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
selectedAssetId = asset.id;
|
||||||
|
updateVisibility(asset, !asset.hidden);
|
||||||
|
});
|
||||||
|
actions.appendChild(toggleBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
row.appendChild(preview);
|
||||||
|
row.appendChild(meta);
|
||||||
|
row.appendChild(actions);
|
||||||
|
|
||||||
|
li.addEventListener("click", () => {
|
||||||
|
selectedAssetId = asset.id;
|
||||||
|
updateRenderState(asset);
|
||||||
|
drawAndList();
|
||||||
|
});
|
||||||
|
|
||||||
|
li.appendChild(row);
|
||||||
|
list.appendChild(li);
|
||||||
|
}
|
||||||
|
|
||||||
function renderAssetList() {
|
function renderAssetList() {
|
||||||
const list = document.getElementById("asset-list");
|
const list = document.getElementById("asset-list");
|
||||||
if (controlsPlaceholder && controlsPanel && controlsPanel.parentElement !== controlsPlaceholder) {
|
if (controlsPlaceholder && controlsPanel && controlsPanel.parentElement !== controlsPlaceholder) {
|
||||||
@@ -1334,99 +1503,20 @@ export function createAdminConsole({
|
|||||||
|
|
||||||
const codeAssets = getCodeAssets();
|
const codeAssets = getCodeAssets();
|
||||||
const audioAssets = getAudioAssets();
|
const audioAssets = getAudioAssets();
|
||||||
const sortedAssets = [...codeAssets, ...audioAssets, ...getAssetsByLayer()];
|
const visualAssets = getAssetsByLayer();
|
||||||
sortedAssets.forEach((asset) => {
|
|
||||||
const li = document.createElement("li");
|
|
||||||
li.className = "asset-item";
|
|
||||||
if (asset.id === selectedAssetId) {
|
|
||||||
li.classList.add("selected");
|
|
||||||
}
|
|
||||||
li.classList.toggle("is-hidden", !!asset.hidden);
|
|
||||||
|
|
||||||
const row = document.createElement("div");
|
if (visualAssets.length) {
|
||||||
row.className = "asset-row";
|
list.appendChild(createSectionHeader("Canvas assets"));
|
||||||
|
visualAssets.forEach((asset) => appendAssetListItem(list, asset));
|
||||||
const preview = createPreviewElement(asset);
|
}
|
||||||
|
if (audioAssets.length) {
|
||||||
const meta = document.createElement("div");
|
list.appendChild(createSectionHeader("Audio assets"));
|
||||||
meta.className = "meta";
|
audioAssets.forEach((asset) => appendAssetListItem(list, asset));
|
||||||
const name = document.createElement("strong");
|
}
|
||||||
name.textContent = asset.name || `Asset ${asset.id.slice(0, 6)}`;
|
if (codeAssets.length) {
|
||||||
const details = document.createElement("small");
|
list.appendChild(createSectionHeader("Script assets (always on top)"));
|
||||||
details.textContent = getAssetTypeLabel(asset);
|
codeAssets.forEach((asset) => appendAssetListItem(list, asset));
|
||||||
meta.appendChild(name);
|
}
|
||||||
meta.appendChild(details);
|
|
||||||
|
|
||||||
const actions = document.createElement("div");
|
|
||||||
actions.className = "actions";
|
|
||||||
|
|
||||||
if (isCodeAsset(asset)) {
|
|
||||||
const editBtn = document.createElement("button");
|
|
||||||
editBtn.type = "button";
|
|
||||||
editBtn.className = "ghost icon-button";
|
|
||||||
editBtn.innerHTML = '<i class="fa-solid fa-code"></i>';
|
|
||||||
editBtn.title = "Edit script";
|
|
||||||
editBtn.addEventListener("click", (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
customAssetModal?.openEditor?.(asset);
|
|
||||||
});
|
|
||||||
actions.appendChild(editBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAudioAsset(asset)) {
|
|
||||||
const playBtn = document.createElement("button");
|
|
||||||
playBtn.type = "button";
|
|
||||||
playBtn.className = "ghost icon-button";
|
|
||||||
const isLooping = !!asset.audioLoop;
|
|
||||||
const isPlayingLoop = getLoopPlaybackState(asset);
|
|
||||||
updatePlayButtonIcon(playBtn, isLooping, isPlayingLoop);
|
|
||||||
playBtn.title = isLooping
|
|
||||||
? isPlayingLoop
|
|
||||||
? "Pause looping audio"
|
|
||||||
: "Play looping audio"
|
|
||||||
: "Play audio";
|
|
||||||
playBtn.addEventListener("click", (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const nextPlay = isLooping
|
|
||||||
? !(loopPlaybackState.get(asset.id) ?? getLoopPlaybackState(asset))
|
|
||||||
: true;
|
|
||||||
if (isLooping) {
|
|
||||||
loopPlaybackState.set(asset.id, nextPlay);
|
|
||||||
updatePlayButtonIcon(playBtn, true, nextPlay);
|
|
||||||
playBtn.title = nextPlay ? "Pause looping audio" : "Play looping audio";
|
|
||||||
}
|
|
||||||
triggerAudioPlayback(asset, nextPlay);
|
|
||||||
});
|
|
||||||
actions.appendChild(playBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAudioAsset(asset) && !isCodeAsset(asset)) {
|
|
||||||
const toggleBtn = document.createElement("button");
|
|
||||||
toggleBtn.type = "button";
|
|
||||||
toggleBtn.className = "ghost icon-button";
|
|
||||||
toggleBtn.innerHTML = `<i class="fa-solid ${asset.hidden ? "fa-eye" : "fa-eye-slash"}"></i>`;
|
|
||||||
toggleBtn.title = asset.hidden ? "Show asset" : "Hide asset";
|
|
||||||
toggleBtn.addEventListener("click", (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
selectedAssetId = asset.id;
|
|
||||||
updateVisibility(asset, !asset.hidden);
|
|
||||||
});
|
|
||||||
actions.appendChild(toggleBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
row.appendChild(preview);
|
|
||||||
row.appendChild(meta);
|
|
||||||
row.appendChild(actions);
|
|
||||||
|
|
||||||
li.addEventListener("click", () => {
|
|
||||||
selectedAssetId = asset.id;
|
|
||||||
updateRenderState(asset);
|
|
||||||
drawAndList();
|
|
||||||
});
|
|
||||||
|
|
||||||
li.appendChild(row);
|
|
||||||
list.appendChild(li);
|
|
||||||
});
|
|
||||||
|
|
||||||
updateSelectedAssetControls();
|
updateSelectedAssetControls();
|
||||||
}
|
}
|
||||||
@@ -1830,6 +1920,11 @@ export function createAdminConsole({
|
|||||||
selectedAssetBadges.innerHTML = "";
|
selectedAssetBadges.innerHTML = "";
|
||||||
if (asset) {
|
if (asset) {
|
||||||
selectedAssetBadges.appendChild(createBadge(getDisplayMediaType(asset)));
|
selectedAssetBadges.appendChild(createBadge(getDisplayMediaType(asset)));
|
||||||
|
if (isCodeAsset(asset)) {
|
||||||
|
selectedAssetBadges.appendChild(createBadge(`Script layer ${getScriptLayerValue(asset.id)}`, "subtle"));
|
||||||
|
} else if (!isAudioAsset(asset)) {
|
||||||
|
selectedAssetBadges.appendChild(createBadge(`Layer ${getLayerValue(asset.id)}`, "subtle"));
|
||||||
|
}
|
||||||
const aspectLabel = !isAudioAsset(asset) && !isCodeAsset(asset) ? formatAspectRatioLabel(asset) : "";
|
const aspectLabel = !isAudioAsset(asset) && !isCodeAsset(asset) ? formatAspectRatioLabel(asset) : "";
|
||||||
if (aspectLabel) {
|
if (aspectLabel) {
|
||||||
selectedAssetBadges.appendChild(createBadge(aspectLabel, "subtle"));
|
selectedAssetBadges.appendChild(createBadge(aspectLabel, "subtle"));
|
||||||
@@ -2028,40 +2123,61 @@ export function createAdminConsole({
|
|||||||
drawAndList();
|
drawAndList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLayeredAssets(asset) {
|
||||||
|
if (!asset) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return isCodeAsset(asset) ? getScriptAssetsByLayer() : getAssetsByLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyOrderForAsset(asset, ordered) {
|
||||||
|
if (!asset) return;
|
||||||
|
if (isCodeAsset(asset)) {
|
||||||
|
applyScriptLayerOrder(ordered);
|
||||||
|
} else {
|
||||||
|
applyLayerOrder(ordered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveLayerItem(asset, direction) {
|
||||||
|
if (!asset) return;
|
||||||
|
const ordered = getLayeredAssets(asset);
|
||||||
|
const index = ordered.findIndex((item) => item.id === asset.id);
|
||||||
|
if (index === -1) return;
|
||||||
|
const nextIndex = direction === "up" ? index - 1 : index + 1;
|
||||||
|
if (nextIndex < 0 || nextIndex >= ordered.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[ordered[index], ordered[nextIndex]] = [ordered[nextIndex], ordered[index]];
|
||||||
|
applyOrderForAsset(asset, ordered);
|
||||||
|
}
|
||||||
|
|
||||||
function bringForward() {
|
function bringForward() {
|
||||||
const asset = getSelectedAsset();
|
const asset = getSelectedAsset();
|
||||||
if (!asset) return;
|
if (!asset || isAudioAsset(asset)) return;
|
||||||
const ordered = getAssetsByLayer();
|
moveLayerItem(asset, "up");
|
||||||
const index = ordered.findIndex((item) => item.id === asset.id);
|
|
||||||
if (index <= 0) return;
|
|
||||||
[ordered[index], ordered[index - 1]] = [ordered[index - 1], ordered[index]];
|
|
||||||
applyLayerOrder(ordered);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function bringBackward() {
|
function bringBackward() {
|
||||||
const asset = getSelectedAsset();
|
const asset = getSelectedAsset();
|
||||||
if (!asset) return;
|
if (!asset || isAudioAsset(asset)) return;
|
||||||
const ordered = getAssetsByLayer();
|
moveLayerItem(asset, "down");
|
||||||
const index = ordered.findIndex((item) => item.id === asset.id);
|
|
||||||
if (index === -1 || index === ordered.length - 1) return;
|
|
||||||
[ordered[index], ordered[index + 1]] = [ordered[index + 1], ordered[index]];
|
|
||||||
applyLayerOrder(ordered);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function bringToFront() {
|
function bringToFront() {
|
||||||
const asset = getSelectedAsset();
|
const asset = getSelectedAsset();
|
||||||
if (!asset) return;
|
if (!asset || isAudioAsset(asset)) return;
|
||||||
const ordered = getAssetsByLayer().filter((item) => item.id !== asset.id);
|
const ordered = getLayeredAssets(asset).filter((item) => item.id !== asset.id);
|
||||||
ordered.unshift(asset);
|
ordered.unshift(asset);
|
||||||
applyLayerOrder(ordered);
|
applyOrderForAsset(asset, ordered);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendToBack() {
|
function sendToBack() {
|
||||||
const asset = getSelectedAsset();
|
const asset = getSelectedAsset();
|
||||||
if (!asset) return;
|
if (!asset || isAudioAsset(asset)) return;
|
||||||
const ordered = getAssetsByLayer().filter((item) => item.id !== asset.id);
|
const ordered = getLayeredAssets(asset).filter((item) => item.id !== asset.id);
|
||||||
ordered.push(asset);
|
ordered.push(asset);
|
||||||
applyLayerOrder(ordered);
|
applyOrderForAsset(asset, ordered);
|
||||||
}
|
}
|
||||||
|
|
||||||
globalThis.handleFileSelection = handleFileSelection;
|
globalThis.handleFileSelection = handleFileSelection;
|
||||||
@@ -2081,6 +2197,14 @@ export function createAdminConsole({
|
|||||||
drawAndList();
|
drawAndList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyScriptLayerOrder(ordered) {
|
||||||
|
const newOrder = ordered.map((item) => item.id).filter((id) => assets.has(id));
|
||||||
|
scriptLayerOrder = newOrder;
|
||||||
|
const changed = ordered.map((item) => assets.get(item.id)).filter(Boolean);
|
||||||
|
changed.forEach((item) => schedulePersistTransform(item, true));
|
||||||
|
drawAndList();
|
||||||
|
}
|
||||||
|
|
||||||
function getAssetAspectRatio(asset) {
|
function getAssetAspectRatio(asset) {
|
||||||
const media = ensureMedia(asset);
|
const media = ensureMedia(asset);
|
||||||
if (isVideoElement(media) && media?.videoWidth && media?.videoHeight) {
|
if (isVideoElement(media) && media?.videoWidth && media?.videoHeight) {
|
||||||
@@ -2202,6 +2326,7 @@ export function createAdminConsole({
|
|||||||
assets.delete(asset.id);
|
assets.delete(asset.id);
|
||||||
renderStates.delete(asset.id);
|
renderStates.delete(asset.id);
|
||||||
layerOrder = layerOrder.filter((id) => id !== asset.id);
|
layerOrder = layerOrder.filter((id) => id !== asset.id);
|
||||||
|
scriptLayerOrder = scriptLayerOrder.filter((id) => id !== asset.id);
|
||||||
cancelPendingTransform(asset.id);
|
cancelPendingTransform(asset.id);
|
||||||
if (selectedAssetId === asset.id) {
|
if (selectedAssetId === asset.id) {
|
||||||
selectedAssetId = null;
|
selectedAssetId = null;
|
||||||
@@ -2319,7 +2444,10 @@ export function createAdminConsole({
|
|||||||
audioPitch: asset.audioPitch,
|
audioPitch: asset.audioPitch,
|
||||||
audioVolume: asset.audioVolume,
|
audioVolume: asset.audioVolume,
|
||||||
};
|
};
|
||||||
if (!isAudioAsset(asset) && !isCodeAsset(asset)) {
|
if (isCodeAsset(asset)) {
|
||||||
|
const layer = getScriptLayerValue(asset.id);
|
||||||
|
payload.zIndex = layer;
|
||||||
|
} else if (!isAudioAsset(asset)) {
|
||||||
const layer = getLayerValue(asset.id);
|
const layer = getLayerValue(asset.id);
|
||||||
payload.x = asset.x;
|
payload.x = asset.x;
|
||||||
payload.y = asset.y;
|
payload.y = asset.y;
|
||||||
|
|||||||
@@ -1,41 +1,83 @@
|
|||||||
import { isVisualAsset } from "./assetKinds.js";
|
import { isCodeAsset, isVisualAsset } from "./assetKinds.js";
|
||||||
|
|
||||||
|
function isScriptAsset(asset) {
|
||||||
|
return isCodeAsset(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLayerableVisual(asset) {
|
||||||
|
return isVisualAsset(asset) && !isScriptAsset(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLayerBucket(state, asset) {
|
||||||
|
if (isScriptAsset(asset)) {
|
||||||
|
if (!Array.isArray(state.scriptLayerOrder)) {
|
||||||
|
state.scriptLayerOrder = [];
|
||||||
|
}
|
||||||
|
return state.scriptLayerOrder;
|
||||||
|
}
|
||||||
|
if (isLayerableVisual(asset)) {
|
||||||
|
return state.layerOrder;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOrder(state, predicate, existing) {
|
||||||
|
const filtered = existing.filter((id) => {
|
||||||
|
const asset = state.assets.get(id);
|
||||||
|
return asset && predicate(asset);
|
||||||
|
});
|
||||||
|
state.assets.forEach((asset, id) => {
|
||||||
|
if (!predicate(asset)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!filtered.includes(id)) {
|
||||||
|
filtered.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
export function ensureLayerPosition(state, assetId, placement = "keep") {
|
export function ensureLayerPosition(state, assetId, placement = "keep") {
|
||||||
const asset = state.assets.get(assetId);
|
const asset = state.assets.get(assetId);
|
||||||
if (asset && !isVisualAsset(asset)) {
|
if (!asset) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const existingIndex = state.layerOrder.indexOf(assetId);
|
const bucket = getLayerBucket(state, asset);
|
||||||
|
if (!bucket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const existingIndex = bucket.indexOf(assetId);
|
||||||
if (existingIndex !== -1 && placement === "keep") {
|
if (existingIndex !== -1 && placement === "keep") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (existingIndex !== -1) {
|
if (existingIndex !== -1) {
|
||||||
state.layerOrder.splice(existingIndex, 1);
|
bucket.splice(existingIndex, 1);
|
||||||
}
|
}
|
||||||
if (placement === "append") {
|
if (placement === "append") {
|
||||||
state.layerOrder.push(assetId);
|
bucket.push(assetId);
|
||||||
} else {
|
} else {
|
||||||
state.layerOrder.unshift(assetId);
|
bucket.unshift(assetId);
|
||||||
|
}
|
||||||
|
if (bucket === state.layerOrder) {
|
||||||
|
state.layerOrder = normalizeOrder(state, isLayerableVisual, bucket);
|
||||||
|
} else {
|
||||||
|
state.scriptLayerOrder = normalizeOrder(state, isScriptAsset, bucket);
|
||||||
}
|
}
|
||||||
state.layerOrder = state.layerOrder.filter((id) => state.assets.has(id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLayerOrder(state) {
|
export function getLayerOrder(state) {
|
||||||
state.layerOrder = state.layerOrder.filter((id) => {
|
state.layerOrder = normalizeOrder(state, isLayerableVisual, state.layerOrder);
|
||||||
const asset = state.assets.get(id);
|
|
||||||
return asset && isVisualAsset(asset);
|
|
||||||
});
|
|
||||||
state.assets.forEach((asset, id) => {
|
|
||||||
if (!isVisualAsset(asset)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!state.layerOrder.includes(id)) {
|
|
||||||
state.layerOrder.unshift(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return state.layerOrder;
|
return state.layerOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getScriptLayerOrder(state) {
|
||||||
|
if (!Array.isArray(state.scriptLayerOrder)) {
|
||||||
|
state.scriptLayerOrder = [];
|
||||||
|
}
|
||||||
|
state.scriptLayerOrder = normalizeOrder(state, isScriptAsset, state.scriptLayerOrder);
|
||||||
|
return state.scriptLayerOrder;
|
||||||
|
}
|
||||||
|
|
||||||
export function getRenderOrder(state) {
|
export function getRenderOrder(state) {
|
||||||
return [...getLayerOrder(state)]
|
return [...getLayerOrder(state)]
|
||||||
.reverse()
|
.reverse()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { AssetKind, MIN_FRAME_TIME, VISIBILITY_THRESHOLD } from "./constants.js";
|
import { AssetKind, MIN_FRAME_TIME, VISIBILITY_THRESHOLD } from "./constants.js";
|
||||||
import { createBroadcastState } from "./state.js";
|
import { createBroadcastState } from "./state.js";
|
||||||
import { getAssetKind, isCodeAsset, isModelAsset, isVisualAsset, isVideoElement } from "./assetKinds.js";
|
import { getAssetKind, isCodeAsset, isModelAsset, isVisualAsset, isVideoElement } from "./assetKinds.js";
|
||||||
import { ensureLayerPosition, getLayerOrder, getRenderOrder } from "./layers.js";
|
import { ensureLayerPosition, getLayerOrder, getRenderOrder, getScriptLayerOrder } from "./layers.js";
|
||||||
import { getVisibilityState, smoothState } from "./visibility.js";
|
import { getVisibilityState, smoothState } from "./visibility.js";
|
||||||
import { createAudioManager } from "./audioManager.js";
|
import { createAudioManager } from "./audioManager.js";
|
||||||
import { createMediaManager } from "./mediaManager.js";
|
import { createMediaManager } from "./mediaManager.js";
|
||||||
@@ -89,6 +89,7 @@ export class BroadcastRenderer {
|
|||||||
|
|
||||||
renderAssets(list) {
|
renderAssets(list) {
|
||||||
this.state.layerOrder = [];
|
this.state.layerOrder = [];
|
||||||
|
this.state.scriptLayerOrder = [];
|
||||||
list.forEach((asset) => {
|
list.forEach((asset) => {
|
||||||
this.storeAsset(asset, "append");
|
this.storeAsset(asset, "append");
|
||||||
if (isCodeAsset(asset)) {
|
if (isCodeAsset(asset)) {
|
||||||
@@ -119,6 +120,7 @@ export class BroadcastRenderer {
|
|||||||
removeAsset(assetId) {
|
removeAsset(assetId) {
|
||||||
this.state.assets.delete(assetId);
|
this.state.assets.delete(assetId);
|
||||||
this.state.layerOrder = this.state.layerOrder.filter((id) => id !== assetId);
|
this.state.layerOrder = this.state.layerOrder.filter((id) => id !== assetId);
|
||||||
|
this.state.scriptLayerOrder = this.state.scriptLayerOrder.filter((id) => id !== assetId);
|
||||||
this.mediaManager.clearMedia(assetId);
|
this.mediaManager.clearMedia(assetId);
|
||||||
this.modelManager.clearModel(assetId);
|
this.modelManager.clearModel(assetId);
|
||||||
this.stopUserJavaScriptWorker(assetId);
|
this.stopUserJavaScriptWorker(assetId);
|
||||||
@@ -284,6 +286,7 @@ export class BroadcastRenderer {
|
|||||||
const merged = this.normalizePayload({ ...existing, ...sanitizedPatch });
|
const merged = this.normalizePayload({ ...existing, ...sanitizedPatch });
|
||||||
console.log(merged);
|
console.log(merged);
|
||||||
const isVisual = isVisualAsset(merged);
|
const isVisual = isVisualAsset(merged);
|
||||||
|
const isScript = isCodeAsset(merged);
|
||||||
if (sanitizedPatch.hidden) {
|
if (sanitizedPatch.hidden) {
|
||||||
this.hideAssetWithTransition(merged);
|
this.hideAssetWithTransition(merged);
|
||||||
return;
|
return;
|
||||||
@@ -293,11 +296,19 @@ export class BroadcastRenderer {
|
|||||||
: Number.isFinite(patch.zIndex)
|
: Number.isFinite(patch.zIndex)
|
||||||
? patch.zIndex
|
? patch.zIndex
|
||||||
: null;
|
: null;
|
||||||
if (isVisual && Number.isFinite(targetLayer)) {
|
if (Number.isFinite(targetLayer)) {
|
||||||
const currentOrder = getLayerOrder(this.state).filter((id) => id !== assetId);
|
if (isScript) {
|
||||||
const insertIndex = Math.max(0, currentOrder.length - Math.round(targetLayer));
|
const currentOrder = getScriptLayerOrder(this.state).filter((id) => id !== assetId);
|
||||||
currentOrder.splice(insertIndex, 0, assetId);
|
const insertIndex = Math.max(0, currentOrder.length - Math.round(targetLayer));
|
||||||
this.state.layerOrder = currentOrder;
|
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));
|
||||||
|
currentOrder.splice(insertIndex, 0, assetId);
|
||||||
|
this.state.layerOrder = currentOrder;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.storeAsset(merged);
|
this.storeAsset(merged);
|
||||||
this.mediaManager.ensureMedia(merged);
|
this.mediaManager.ensureMedia(merged);
|
||||||
@@ -767,6 +778,7 @@ export class BroadcastRenderer {
|
|||||||
}
|
}
|
||||||
const existing = this.scriptCanvases.get(assetId);
|
const existing = this.scriptCanvases.get(assetId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
this.applyScriptCanvasOrder();
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
@@ -775,6 +787,7 @@ export class BroadcastRenderer {
|
|||||||
this.applyScriptCanvasSize(canvas);
|
this.applyScriptCanvasSize(canvas);
|
||||||
this.scriptLayer.appendChild(canvas);
|
this.scriptLayer.appendChild(canvas);
|
||||||
this.scriptCanvases.set(assetId, canvas);
|
this.scriptCanvases.set(assetId, canvas);
|
||||||
|
this.applyScriptCanvasOrder();
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -785,12 +798,14 @@ export class BroadcastRenderer {
|
|||||||
}
|
}
|
||||||
canvas.remove();
|
canvas.remove();
|
||||||
this.scriptCanvases.delete(assetId);
|
this.scriptCanvases.delete(assetId);
|
||||||
|
this.applyScriptCanvasOrder();
|
||||||
}
|
}
|
||||||
|
|
||||||
resizeScriptCanvases() {
|
resizeScriptCanvases() {
|
||||||
this.scriptCanvases.forEach((canvas) => {
|
this.scriptCanvases.forEach((canvas) => {
|
||||||
this.applyScriptCanvasSize(canvas);
|
this.applyScriptCanvasSize(canvas);
|
||||||
});
|
});
|
||||||
|
this.applyScriptCanvasOrder();
|
||||||
}
|
}
|
||||||
|
|
||||||
applyScriptCanvasSize(canvas) {
|
applyScriptCanvasSize(canvas) {
|
||||||
@@ -807,6 +822,21 @@ export class BroadcastRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyScriptCanvasOrder() {
|
||||||
|
if (!this.scriptLayer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ordered = getScriptLayerOrder(this.state);
|
||||||
|
ordered.forEach((id, index) => {
|
||||||
|
const canvas = this.scriptCanvases.get(id);
|
||||||
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
canvas.style.zIndex = `${ordered.length - index}`;
|
||||||
|
this.scriptLayer.appendChild(canvas);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async resolveScriptAttachments(attachments) {
|
async resolveScriptAttachments(attachments) {
|
||||||
if (!Array.isArray(attachments) || attachments.length === 0) {
|
if (!Array.isArray(attachments) || attachments.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ export function createBroadcastState() {
|
|||||||
animationFailures: new Map(),
|
animationFailures: new Map(),
|
||||||
videoPlaybackStates: new WeakMap(),
|
videoPlaybackStates: new WeakMap(),
|
||||||
layerOrder: [],
|
layerOrder: [],
|
||||||
|
scriptLayerOrder: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,6 +251,7 @@
|
|||||||
onclick="sendToBack()"
|
onclick="sendToBack()"
|
||||||
class="secondary"
|
class="secondary"
|
||||||
title="Send to back"
|
title="Send to back"
|
||||||
|
data-code-enabled="true"
|
||||||
>
|
>
|
||||||
<i class="fa-solid fa-angles-down"></i>
|
<i class="fa-solid fa-angles-down"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -259,6 +260,7 @@
|
|||||||
onclick="bringBackward()"
|
onclick="bringBackward()"
|
||||||
class="secondary"
|
class="secondary"
|
||||||
title="Move backward"
|
title="Move backward"
|
||||||
|
data-code-enabled="true"
|
||||||
>
|
>
|
||||||
<i class="fa-solid fa-arrow-down"></i>
|
<i class="fa-solid fa-arrow-down"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -267,6 +269,7 @@
|
|||||||
onclick="bringForward()"
|
onclick="bringForward()"
|
||||||
class="secondary"
|
class="secondary"
|
||||||
title="Move forward"
|
title="Move forward"
|
||||||
|
data-code-enabled="true"
|
||||||
>
|
>
|
||||||
<i class="fa-solid fa-arrow-up"></i>
|
<i class="fa-solid fa-arrow-up"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -275,6 +278,7 @@
|
|||||||
onclick="bringToFront()"
|
onclick="bringToFront()"
|
||||||
class="secondary"
|
class="secondary"
|
||||||
title="Bring to front"
|
title="Bring to front"
|
||||||
|
data-code-enabled="true"
|
||||||
>
|
>
|
||||||
<i class="fa-solid fa-angles-up"></i>
|
<i class="fa-solid fa-angles-up"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user