mirror of
https://github.com/imgfloat/server.git
synced 2026-03-23 07:10:38 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45fb1921da | |||
| ed5007538b | |||
| 0f088dc83b |
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>0.0.3</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>
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package dev.kruhlmann.imgfloat.controller;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.Locale;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.messaging.handler.annotation.DestinationVariable;
|
||||||
|
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||||
|
import org.springframework.messaging.handler.annotation.Payload;
|
||||||
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
|
||||||
|
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
|
||||||
|
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
|
||||||
|
import dev.kruhlmann.imgfloat.model.api.response.AssetEvent;
|
||||||
|
import dev.kruhlmann.imgfloat.service.AuthorizationService;
|
||||||
|
import dev.kruhlmann.imgfloat.service.ChannelDirectoryService;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class ChannelPreviewWsController {
|
||||||
|
|
||||||
|
private final ChannelDirectoryService channelDirectoryService;
|
||||||
|
private final AuthorizationService authorizationService;
|
||||||
|
private final SimpMessagingTemplate messagingTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public ChannelPreviewWsController(
|
||||||
|
ChannelDirectoryService channelDirectoryService,
|
||||||
|
AuthorizationService authorizationService,
|
||||||
|
SimpMessagingTemplate messagingTemplate
|
||||||
|
) {
|
||||||
|
this.channelDirectoryService = channelDirectoryService;
|
||||||
|
this.authorizationService = authorizationService;
|
||||||
|
this.messagingTemplate = messagingTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@MessageMapping("/channel/{broadcaster}/assets/{assetId}/preview")
|
||||||
|
public void previewTransform(
|
||||||
|
@DestinationVariable String broadcaster,
|
||||||
|
@DestinationVariable String assetId,
|
||||||
|
@Payload @Valid TransformRequest request,
|
||||||
|
Principal principal
|
||||||
|
) {
|
||||||
|
String sessionUsername = sessionUsername(principal);
|
||||||
|
authorizationService.userIsBroadcasterOrChannelAdminForBroadcasterOrThrowHttpError(
|
||||||
|
broadcaster,
|
||||||
|
sessionUsername
|
||||||
|
);
|
||||||
|
channelDirectoryService
|
||||||
|
.previewTransform(broadcaster, assetId, request)
|
||||||
|
.ifPresent((patch) -> messagingTemplate.convertAndSend(
|
||||||
|
topicFor(broadcaster),
|
||||||
|
AssetEvent.preview(broadcaster, assetId, patch)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sessionUsername(Principal principal) {
|
||||||
|
if (principal instanceof OAuth2AuthenticationToken token) {
|
||||||
|
OauthSessionUser user = OauthSessionUser.from(token);
|
||||||
|
return user == null ? null : user.login();
|
||||||
|
}
|
||||||
|
return principal == null ? null : principal.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String topicFor(String broadcaster) {
|
||||||
|
return "/topic/channel/" + broadcaster.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ public class AssetEvent {
|
|||||||
UPDATED,
|
UPDATED,
|
||||||
VISIBILITY,
|
VISIBILITY,
|
||||||
PLAY,
|
PLAY,
|
||||||
|
PREVIEW,
|
||||||
DELETED,
|
DELETED,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +68,15 @@ public class AssetEvent {
|
|||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static AssetEvent preview(String channel, String assetId, AssetPatch patch) {
|
||||||
|
AssetEvent event = new AssetEvent();
|
||||||
|
event.type = Type.PREVIEW;
|
||||||
|
event.channel = channel;
|
||||||
|
event.patch = patch;
|
||||||
|
event.assetId = assetId;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
public static AssetEvent deleted(String channel, String assetId) {
|
public static AssetEvent deleted(String channel, String assetId) {
|
||||||
AssetEvent event = new AssetEvent();
|
AssetEvent event = new AssetEvent();
|
||||||
event.type = Type.DELETED;
|
event.type = Type.DELETED;
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ public class ChannelDirectoryService {
|
|||||||
private static final String DEFAULT_CODE_MEDIA_TYPE = "application/javascript";
|
private static final String DEFAULT_CODE_MEDIA_TYPE = "application/javascript";
|
||||||
private static final int MAX_ALLOWED_SCRIPT_DOMAINS = 32;
|
private static final int MAX_ALLOWED_SCRIPT_DOMAINS = 32;
|
||||||
private static final Pattern ALLOWED_DOMAIN_PATTERN = Pattern.compile("^[a-z0-9.-]+(?::[0-9]{1,5})?$");
|
private static final Pattern ALLOWED_DOMAIN_PATTERN = Pattern.compile("^[a-z0-9.-]+(?::[0-9]{1,5})?$");
|
||||||
|
private static final EnumSet<AssetType> VISUAL_ASSET_TYPES = EnumSet.of(
|
||||||
|
AssetType.IMAGE,
|
||||||
|
AssetType.VIDEO,
|
||||||
|
AssetType.MODEL,
|
||||||
|
AssetType.OTHER
|
||||||
|
);
|
||||||
|
|
||||||
private final ChannelRepository channelRepository;
|
private final ChannelRepository channelRepository;
|
||||||
private final AssetRepository assetRepository;
|
private final AssetRepository assetRepository;
|
||||||
@@ -1164,6 +1170,75 @@ public class ChannelDirectoryService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<AssetPatch> previewTransform(String broadcaster, String assetId, TransformRequest request) {
|
||||||
|
String normalized = normalize(broadcaster);
|
||||||
|
|
||||||
|
Asset asset = assetRepository
|
||||||
|
.findById(assetId)
|
||||||
|
.filter((stored) -> normalized.equals(stored.getBroadcaster()))
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset not found"));
|
||||||
|
|
||||||
|
if (!VISUAL_ASSET_TYPES.contains(asset.getAssetType())) {
|
||||||
|
throw new ResponseStatusException(BAD_REQUEST, "Asset is not visual");
|
||||||
|
}
|
||||||
|
|
||||||
|
VisualAsset visual = visualAssetRepository
|
||||||
|
.findById(asset.getId())
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not visual"));
|
||||||
|
|
||||||
|
TransformRequest previewRequest = copyVisualTransformRequest(request);
|
||||||
|
validateVisualBounds(previewRequest);
|
||||||
|
|
||||||
|
AssetPatch.VisualSnapshot before = new AssetPatch.VisualSnapshot(
|
||||||
|
visual.getX(),
|
||||||
|
visual.getY(),
|
||||||
|
visual.getWidth(),
|
||||||
|
visual.getHeight(),
|
||||||
|
visual.getRotation(),
|
||||||
|
visual.getSpeed(),
|
||||||
|
visual.isMuted(),
|
||||||
|
displayOrderValue(asset),
|
||||||
|
visual.getAudioVolume()
|
||||||
|
);
|
||||||
|
|
||||||
|
VisualAsset previewState = new VisualAsset();
|
||||||
|
previewState.setId(visual.getId());
|
||||||
|
previewState.setName(visual.getName());
|
||||||
|
previewState.setX(visual.getX());
|
||||||
|
previewState.setY(visual.getY());
|
||||||
|
previewState.setWidth(visual.getWidth());
|
||||||
|
previewState.setHeight(visual.getHeight());
|
||||||
|
previewState.setRotation(visual.getRotation());
|
||||||
|
previewState.setSpeed(visual.getSpeed());
|
||||||
|
previewState.setMuted(visual.isMuted());
|
||||||
|
previewState.setAudioVolume(visual.getAudioVolume());
|
||||||
|
|
||||||
|
if (previewRequest.getX() != null) {
|
||||||
|
previewState.setX(previewRequest.getX());
|
||||||
|
}
|
||||||
|
if (previewRequest.getY() != null) {
|
||||||
|
previewState.setY(previewRequest.getY());
|
||||||
|
}
|
||||||
|
if (previewRequest.getWidth() != null) {
|
||||||
|
previewState.setWidth(previewRequest.getWidth());
|
||||||
|
}
|
||||||
|
if (previewRequest.getHeight() != null) {
|
||||||
|
previewState.setHeight(previewRequest.getHeight());
|
||||||
|
}
|
||||||
|
if (previewRequest.getRotation() != null) {
|
||||||
|
previewState.setRotation(previewRequest.getRotation());
|
||||||
|
}
|
||||||
|
if (previewRequest.getSpeed() != null) {
|
||||||
|
previewState.setSpeed(previewRequest.getSpeed());
|
||||||
|
}
|
||||||
|
if (previewRequest.getMuted() != null) {
|
||||||
|
previewState.setMuted(previewRequest.getMuted());
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetPatch patch = AssetPatch.fromVisualTransform(before, previewState, previewRequest);
|
||||||
|
return hasPatchChanges(patch) ? Optional.of(patch) : Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void reorderAssets(
|
public void reorderAssets(
|
||||||
String broadcaster,
|
String broadcaster,
|
||||||
@@ -1288,6 +1363,16 @@ public class ChannelDirectoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void validateVisualTransform(TransformRequest req) {
|
private void validateVisualTransform(TransformRequest req) {
|
||||||
|
validateVisualBounds(req);
|
||||||
|
if (req.getOrder() != null && req.getOrder() < 1) {
|
||||||
|
throw new ResponseStatusException(BAD_REQUEST, "Order must be >= 1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateVisualBounds(TransformRequest req) {
|
||||||
|
if (req == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Settings settings = settingsService.get();
|
Settings settings = settingsService.get();
|
||||||
double maxSpeed = settings.getMaxAssetPlaybackSpeedFraction();
|
double maxSpeed = settings.getMaxAssetPlaybackSpeedFraction();
|
||||||
double minSpeed = settings.getMinAssetPlaybackSpeedFraction();
|
double minSpeed = settings.getMinAssetPlaybackSpeedFraction();
|
||||||
@@ -1297,30 +1382,49 @@ public class ChannelDirectoryService {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
req.getWidth() != null && (req.getWidth() <= 0 || req.getWidth() > canvasMaxSizePixels)
|
req.getWidth() != null && (req.getWidth() <= 0 || req.getWidth() > canvasMaxSizePixels)
|
||||||
) throw new ResponseStatusException(
|
) {
|
||||||
|
throw new ResponseStatusException(
|
||||||
BAD_REQUEST,
|
BAD_REQUEST,
|
||||||
"Canvas width out of range [0 to " + canvasMaxSizePixels + "]"
|
"Canvas width out of range [0 to " + canvasMaxSizePixels + "]"
|
||||||
);
|
);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
req.getHeight() != null && (req.getHeight() <= 0 || req.getHeight() > canvasMaxSizePixels)
|
req.getHeight() != null && (req.getHeight() <= 0 || req.getHeight() > canvasMaxSizePixels)
|
||||||
) throw new ResponseStatusException(
|
) {
|
||||||
|
throw new ResponseStatusException(
|
||||||
BAD_REQUEST,
|
BAD_REQUEST,
|
||||||
"Canvas height out of range [0 to " + canvasMaxSizePixels + "]"
|
"Canvas height out of range [0 to " + canvasMaxSizePixels + "]"
|
||||||
);
|
);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
req.getSpeed() != null && (req.getSpeed() < minSpeed || req.getSpeed() > maxSpeed)
|
req.getSpeed() != null && (req.getSpeed() < minSpeed || req.getSpeed() > maxSpeed)
|
||||||
) throw new ResponseStatusException(BAD_REQUEST, "Speed out of range [" + minSpeed + " to " + maxSpeed + "]");
|
) {
|
||||||
if (req.getOrder() != null && req.getOrder() < 1) throw new ResponseStatusException(
|
throw new ResponseStatusException(BAD_REQUEST, "Speed out of range [" + minSpeed + " to " + maxSpeed + "]");
|
||||||
BAD_REQUEST,
|
}
|
||||||
"Order must be >= 1"
|
|
||||||
);
|
|
||||||
if (
|
if (
|
||||||
req.getAudioVolume() != null && (req.getAudioVolume() < minVolume || req.getAudioVolume() > maxVolume)
|
req.getAudioVolume() != null && (req.getAudioVolume() < minVolume || req.getAudioVolume() > maxVolume)
|
||||||
) throw new ResponseStatusException(
|
) {
|
||||||
|
throw new ResponseStatusException(
|
||||||
BAD_REQUEST,
|
BAD_REQUEST,
|
||||||
"Audio volume out of range [" + minVolume + " to " + maxVolume + "]"
|
"Audio volume out of range [" + minVolume + " to " + maxVolume + "]"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TransformRequest copyVisualTransformRequest(TransformRequest source) {
|
||||||
|
TransformRequest copy = new TransformRequest();
|
||||||
|
if (source == null) {
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
copy.setX(source.getX());
|
||||||
|
copy.setY(source.getY());
|
||||||
|
copy.setWidth(source.getWidth());
|
||||||
|
copy.setHeight(source.getHeight());
|
||||||
|
copy.setRotation(source.getRotation());
|
||||||
|
copy.setSpeed(source.getSpeed());
|
||||||
|
copy.setMuted(source.getMuted());
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
private void validateAudioTransform(TransformRequest req) {
|
private void validateAudioTransform(TransformRequest req) {
|
||||||
Settings settings = settingsService.get();
|
Settings settings = settingsService.get();
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ export function createAdminConsole({
|
|||||||
const previewCache = new Map();
|
const previewCache = new Map();
|
||||||
const previewImageCache = new Map();
|
const previewImageCache = new Map();
|
||||||
const pendingTransformSaves = new Map();
|
const pendingTransformSaves = new Map();
|
||||||
|
const livePreviewQueue = new Map();
|
||||||
|
const livePreviewLastSent = new Map();
|
||||||
|
let livePreviewFrameScheduled = false;
|
||||||
const HANDLE_SIZE = 10;
|
const HANDLE_SIZE = 10;
|
||||||
const ROTATE_HANDLE_OFFSET = 32;
|
const ROTATE_HANDLE_OFFSET = 32;
|
||||||
const VOLUME_SLIDER_MAX = SETTINGS.maxAssetVolumeFraction * 100;
|
const VOLUME_SLIDER_MAX = SETTINGS.maxAssetVolumeFraction * 100;
|
||||||
@@ -181,6 +184,51 @@ export function createAdminConsole({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requestLiveTransform(asset) {
|
||||||
|
if (!asset?.id || isAudioAsset(asset)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = buildTransformPayload(asset);
|
||||||
|
if (!payload || !Object.keys(payload).length) {
|
||||||
|
livePreviewQueue.delete(asset.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const serialized = JSON.stringify(payload);
|
||||||
|
if (livePreviewLastSent.get(asset.id) === serialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
livePreviewQueue.set(asset.id, { payload, serialized });
|
||||||
|
requestLivePreviewFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelLiveTransform(assetId) {
|
||||||
|
livePreviewQueue.delete(assetId);
|
||||||
|
livePreviewLastSent.delete(assetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestLivePreviewFrame() {
|
||||||
|
if (livePreviewFrameScheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
livePreviewFrameScheduled = true;
|
||||||
|
requestAnimationFrame(sendQueuedLiveTransforms);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendQueuedLiveTransforms() {
|
||||||
|
livePreviewFrameScheduled = false;
|
||||||
|
if (!livePreviewQueue.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!stompClient || (typeof stompClient.connected === "boolean" && !stompClient.connected)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const [assetId, { payload, serialized }] of livePreviewQueue.entries()) {
|
||||||
|
stompClient.send(`/app/channel/${broadcaster}/assets/${assetId}/preview`, {}, JSON.stringify(payload));
|
||||||
|
livePreviewLastSent.set(assetId, serialized);
|
||||||
|
}
|
||||||
|
livePreviewQueue.clear();
|
||||||
|
}
|
||||||
|
|
||||||
function ensureLayerPosition(assetId, placement = "keep") {
|
function ensureLayerPosition(assetId, placement = "keep") {
|
||||||
ensureLayerPositionForState(layerState, assetId, placement);
|
ensureLayerPositionForState(layerState, assetId, placement);
|
||||||
}
|
}
|
||||||
@@ -522,6 +570,9 @@ export function createAdminConsole({
|
|||||||
handleEvent(body);
|
handleEvent(body);
|
||||||
});
|
});
|
||||||
fetchAssets();
|
fetchAssets();
|
||||||
|
if (livePreviewQueue.size) {
|
||||||
|
requestLivePreviewFrame();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.warn("WebSocket connection issue", error);
|
console.warn("WebSocket connection issue", error);
|
||||||
@@ -675,6 +726,11 @@ export function createAdminConsole({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const assetId = event.assetId || event?.patch?.id || event?.payload?.id;
|
const assetId = event.assetId || event?.patch?.id || event?.payload?.id;
|
||||||
|
if (event.type === "PREVIEW" && event.patch) {
|
||||||
|
applyPreviewPatch(assetId, event.patch);
|
||||||
|
drawAndList(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
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);
|
||||||
@@ -684,6 +740,7 @@ export function createAdminConsole({
|
|||||||
transformBaseline.delete(assetId);
|
transformBaseline.delete(assetId);
|
||||||
loopPlaybackState.delete(assetId);
|
loopPlaybackState.delete(assetId);
|
||||||
cancelPendingTransform(assetId);
|
cancelPendingTransform(assetId);
|
||||||
|
cancelLiveTransform(assetId);
|
||||||
if (selectedAssetId === assetId) {
|
if (selectedAssetId === assetId) {
|
||||||
setSelectedAssetId(null);
|
setSelectedAssetId(null);
|
||||||
}
|
}
|
||||||
@@ -708,6 +765,9 @@ export function createAdminConsole({
|
|||||||
if (!event) {
|
if (!event) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (event.type === "PREVIEW") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const { type, payload, patch } = event;
|
const { type, payload, patch } = event;
|
||||||
if (type === "DELETED" || type === "VISIBILITY") {
|
if (type === "DELETED" || type === "VISIBILITY") {
|
||||||
return true;
|
return true;
|
||||||
@@ -761,6 +821,43 @@ export function createAdminConsole({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyPreviewPatch(assetId, patch) {
|
||||||
|
if (!assetId || !patch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const existing = assets.get(assetId);
|
||||||
|
if (!existing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const merged = { ...existing, ...patch };
|
||||||
|
const isAudio = isAudioAsset(merged);
|
||||||
|
const isScript = isCodeAsset(merged);
|
||||||
|
if (patch.hidden) {
|
||||||
|
clearMedia(assetId);
|
||||||
|
loopPlaybackState.delete(assetId);
|
||||||
|
}
|
||||||
|
const targetOrder = Number.isFinite(patch.order) ? patch.order : null;
|
||||||
|
if (!isAudio && Number.isFinite(targetOrder)) {
|
||||||
|
if (isScript) {
|
||||||
|
const currentOrder = getScriptLayerOrder().filter((id) => id !== assetId);
|
||||||
|
const totalCount = currentOrder.length + 1;
|
||||||
|
const insertIndex = Math.max(0, Math.min(currentOrder.length, totalCount - Math.round(targetOrder)));
|
||||||
|
currentOrder.splice(insertIndex, 0, assetId);
|
||||||
|
scriptLayerOrder = currentOrder;
|
||||||
|
} else {
|
||||||
|
const currentOrder = getLayerOrder().filter((id) => id !== assetId);
|
||||||
|
const totalCount = currentOrder.length + 1;
|
||||||
|
const insertIndex = Math.max(0, Math.min(currentOrder.length, totalCount - Math.round(targetOrder)));
|
||||||
|
currentOrder.splice(insertIndex, 0, assetId);
|
||||||
|
layerOrder = currentOrder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assets.set(assetId, merged);
|
||||||
|
if (!isAudio) {
|
||||||
|
updateRenderState(merged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function markListDirty() {
|
function markListDirty() {
|
||||||
listNeedsRender = true;
|
listNeedsRender = true;
|
||||||
}
|
}
|
||||||
@@ -1078,6 +1175,7 @@ export function createAdminConsole({
|
|||||||
asset.width = nextWidth;
|
asset.width = nextWidth;
|
||||||
asset.height = nextHeight;
|
asset.height = nextHeight;
|
||||||
updateRenderState(asset);
|
updateRenderState(asset);
|
||||||
|
requestLiveTransform(asset);
|
||||||
requestDraw();
|
requestDraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2469,6 +2567,7 @@ export function createAdminConsole({
|
|||||||
layerOrder = layerOrder.filter((id) => id !== asset.id);
|
layerOrder = layerOrder.filter((id) => id !== asset.id);
|
||||||
scriptLayerOrder = scriptLayerOrder.filter((id) => id !== asset.id);
|
scriptLayerOrder = scriptLayerOrder.filter((id) => id !== asset.id);
|
||||||
cancelPendingTransform(asset.id);
|
cancelPendingTransform(asset.id);
|
||||||
|
cancelLiveTransform(asset.id);
|
||||||
if (selectedAssetId === asset.id) {
|
if (selectedAssetId === asset.id) {
|
||||||
setSelectedAssetId(null);
|
setSelectedAssetId(null);
|
||||||
}
|
}
|
||||||
@@ -2605,6 +2704,7 @@ export function createAdminConsole({
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
cancelPendingTransform(asset.id);
|
cancelPendingTransform(asset.id);
|
||||||
|
cancelLiveTransform(asset.id);
|
||||||
const payload = buildTransformPayload(asset);
|
const payload = buildTransformPayload(asset);
|
||||||
if (!Object.keys(payload).length) {
|
if (!Object.keys(payload).length) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@@ -2749,6 +2849,7 @@ export function createAdminConsole({
|
|||||||
asset.y = point.y - interactionState.offsetY;
|
asset.y = point.y - interactionState.offsetY;
|
||||||
updateRenderState(asset);
|
updateRenderState(asset);
|
||||||
canvas.style.cursor = "grabbing";
|
canvas.style.cursor = "grabbing";
|
||||||
|
requestLiveTransform(asset);
|
||||||
requestDraw();
|
requestDraw();
|
||||||
} else if (interactionState.mode === "resize") {
|
} else if (interactionState.mode === "resize") {
|
||||||
resizeFromHandle(interactionState, point);
|
resizeFromHandle(interactionState, point);
|
||||||
@@ -2758,6 +2859,7 @@ export function createAdminConsole({
|
|||||||
asset.rotation = (interactionState.startRotation || 0) + (angle - interactionState.startAngle);
|
asset.rotation = (interactionState.startRotation || 0) + (angle - interactionState.startAngle);
|
||||||
updateRenderState(asset);
|
updateRenderState(asset);
|
||||||
canvas.style.cursor = "grabbing";
|
canvas.style.cursor = "grabbing";
|
||||||
|
requestLiveTransform(asset);
|
||||||
requestDraw();
|
requestDraw();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2771,6 +2873,7 @@ export function createAdminConsole({
|
|||||||
canvas.style.cursor = "default";
|
canvas.style.cursor = "default";
|
||||||
drawAndList();
|
drawAndList();
|
||||||
if (asset) {
|
if (asset) {
|
||||||
|
cancelLiveTransform(asset.id);
|
||||||
persistTransform(asset);
|
persistTransform(asset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,6 +177,11 @@ export class BroadcastRenderer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const assetId = event.assetId || event?.patch?.id || event?.payload?.id;
|
const assetId = event.assetId || event?.patch?.id || event?.payload?.id;
|
||||||
|
if (event.type === "PREVIEW" && event.patch) {
|
||||||
|
this.applyPreviewPatch(assetId, event.patch);
|
||||||
|
this.draw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.type === "VISIBILITY") {
|
if (event.type === "VISIBILITY") {
|
||||||
this.handleVisibilityEvent(event);
|
this.handleVisibilityEvent(event);
|
||||||
return;
|
return;
|
||||||
@@ -276,15 +281,12 @@ export class BroadcastRenderer {
|
|||||||
if (!assetId || !patch) {
|
if (!assetId || !patch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sanitizedPatch = Object.fromEntries(
|
const sanitizedPatch = this.sanitizePatch(patch);
|
||||||
Object.entries(patch).filter(([, value]) => value !== null && value !== undefined),
|
|
||||||
);
|
|
||||||
const existing = this.state.assets.get(assetId);
|
const existing = this.state.assets.get(assetId);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const merged = this.normalizePayload({ ...existing, ...sanitizedPatch });
|
const merged = this.normalizePayload({ ...existing, ...sanitizedPatch });
|
||||||
console.log(merged);
|
|
||||||
const isVisual = isVisualAsset(merged);
|
const isVisual = isVisualAsset(merged);
|
||||||
const isScript = isCodeAsset(merged);
|
const isScript = isCodeAsset(merged);
|
||||||
if (sanitizedPatch.hidden) {
|
if (sanitizedPatch.hidden) {
|
||||||
@@ -316,6 +318,54 @@ export class BroadcastRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyPreviewPatch(assetId, patch) {
|
||||||
|
if (!assetId || !patch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sanitizedPatch = this.sanitizePatch(patch);
|
||||||
|
if (!Object.keys(sanitizedPatch).length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const existing = this.state.assets.get(assetId);
|
||||||
|
if (!existing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const merged = this.normalizePayload({ ...existing, ...sanitizedPatch });
|
||||||
|
if (sanitizedPatch.hidden) {
|
||||||
|
this.hideAssetWithTransition(merged);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isVisual = isVisualAsset(merged);
|
||||||
|
const isScript = isCodeAsset(merged);
|
||||||
|
const targetOrder = Number.isFinite(sanitizedPatch.order) ? sanitizedPatch.order : null;
|
||||||
|
if (Number.isFinite(targetOrder)) {
|
||||||
|
if (isScript) {
|
||||||
|
const currentOrder = getScriptLayerOrder(this.state).filter((id) => id !== assetId);
|
||||||
|
const totalCount = currentOrder.length + 1;
|
||||||
|
const insertIndex = Math.max(0, Math.min(currentOrder.length, totalCount - 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 totalCount = currentOrder.length + 1;
|
||||||
|
const insertIndex = Math.max(0, Math.min(currentOrder.length, totalCount - Math.round(targetOrder)));
|
||||||
|
currentOrder.splice(insertIndex, 0, assetId);
|
||||||
|
this.state.layerOrder = currentOrder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.state.assets.set(assetId, merged);
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitizePatch(patch) {
|
||||||
|
if (!patch) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(patch).filter(([, value]) => value !== null && value !== undefined),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
if (this.frameScheduled) {
|
if (this.frameScheduled) {
|
||||||
this.pendingDraw = true;
|
this.pendingDraw = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user