mirror of
https://github.com/imgfloat/server.git
synced 2026-03-22 23:10:38 +00:00
Fix ordering
This commit is contained in:
@@ -9,6 +9,7 @@ import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
|
|||||||
import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.CodeAssetRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.CodeAssetRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
|
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
|
||||||
|
import dev.kruhlmann.imgfloat.model.api.request.AssetOrderRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.PlaybackRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.PlaybackRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.response.ScriptAssetAttachmentView;
|
import dev.kruhlmann.imgfloat.model.api.response.ScriptAssetAttachmentView;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
|
||||||
@@ -333,6 +334,24 @@ public class ChannelApiController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/assets/order")
|
||||||
|
public ResponseEntity<Void> reorderAssets(
|
||||||
|
@PathVariable("broadcaster") String broadcaster,
|
||||||
|
@Valid @RequestBody AssetOrderRequest request,
|
||||||
|
OAuth2AuthenticationToken oauthToken
|
||||||
|
) {
|
||||||
|
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||||
|
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
|
||||||
|
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
||||||
|
authorizationService.userIsBroadcasterOrChannelAdminForBroadcasterOrThrowHttpError(
|
||||||
|
broadcaster,
|
||||||
|
sessionUsername
|
||||||
|
);
|
||||||
|
LOG.debug("Reordering assets for {} by {}", logBroadcaster, logSessionUsername);
|
||||||
|
channelDirectoryService.reorderAssets(broadcaster, request.getUpdates(), sessionUsername);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/assets/{assetId}/play")
|
@PostMapping("/assets/{assetId}/play")
|
||||||
public ResponseEntity<AssetView> play(
|
public ResponseEntity<AssetView> play(
|
||||||
@PathVariable("broadcaster") String broadcaster,
|
@PathVariable("broadcaster") String broadcaster,
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package dev.kruhlmann.imgfloat.model.api.request;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class AssetOrderRequest {
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotEmpty
|
||||||
|
private List<AssetOrderUpdate> updates;
|
||||||
|
|
||||||
|
public List<AssetOrderUpdate> getUpdates() {
|
||||||
|
return updates;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdates(List<AssetOrderUpdate> updates) {
|
||||||
|
this.updates = updates;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static record AssetOrderUpdate(
|
||||||
|
@NotBlank
|
||||||
|
String assetId,
|
||||||
|
@NotNull
|
||||||
|
Integer order
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -4,9 +4,11 @@ import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
|||||||
import static org.springframework.http.HttpStatus.PAYLOAD_TOO_LARGE;
|
import static org.springframework.http.HttpStatus.PAYLOAD_TOO_LARGE;
|
||||||
|
|
||||||
import dev.kruhlmann.imgfloat.model.AssetType;
|
import dev.kruhlmann.imgfloat.model.AssetType;
|
||||||
|
import dev.kruhlmann.imgfloat.model.api.request.AssetOrderRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.CodeAssetRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.CodeAssetRequest;
|
||||||
|
import dev.kruhlmann.imgfloat.model.api.request.AssetOrderRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.PlaybackRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.PlaybackRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.VisibilityRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.VisibilityRequest;
|
||||||
@@ -1162,6 +1164,129 @@ public class ChannelDirectoryService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void reorderAssets(
|
||||||
|
String broadcaster,
|
||||||
|
List<AssetOrderRequest.AssetOrderUpdate> updates,
|
||||||
|
String actor
|
||||||
|
) {
|
||||||
|
if (updates == null || updates.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String normalized = normalize(broadcaster);
|
||||||
|
applyBulkOrderUpdates(
|
||||||
|
broadcaster,
|
||||||
|
normalized,
|
||||||
|
updates,
|
||||||
|
EnumSet.of(AssetType.SCRIPT),
|
||||||
|
actor,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
applyBulkOrderUpdates(
|
||||||
|
broadcaster,
|
||||||
|
normalized,
|
||||||
|
updates,
|
||||||
|
EnumSet.of(AssetType.IMAGE, AssetType.VIDEO, AssetType.MODEL, AssetType.OTHER),
|
||||||
|
actor,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyBulkOrderUpdates(
|
||||||
|
String broadcaster,
|
||||||
|
String normalized,
|
||||||
|
List<AssetOrderRequest.AssetOrderUpdate> updates,
|
||||||
|
EnumSet<AssetType> types,
|
||||||
|
String actor,
|
||||||
|
boolean script
|
||||||
|
) {
|
||||||
|
if (updates == null || updates.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Asset> bucket = assetRepository
|
||||||
|
.findByBroadcaster(normalized)
|
||||||
|
.stream()
|
||||||
|
.filter((asset) -> types.contains(asset.getAssetType()))
|
||||||
|
.sorted(
|
||||||
|
Comparator.comparingInt(this::displayOrderValue)
|
||||||
|
.reversed()
|
||||||
|
.thenComparing(Asset::getCreatedAt, Comparator.nullsFirst(Comparator.naturalOrder()))
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
if (bucket.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, Integer> desiredOrder = new HashMap<>();
|
||||||
|
Set<String> bucketIds = bucket.stream().map(Asset::getId).collect(Collectors.toSet());
|
||||||
|
for (AssetOrderRequest.AssetOrderUpdate update : updates) {
|
||||||
|
if (update == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String assetId = update.assetId();
|
||||||
|
if (!bucketIds.contains(assetId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Integer order = update.order();
|
||||||
|
if (order == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (order < 1) {
|
||||||
|
throw new ResponseStatusException(BAD_REQUEST, "Order must be >= 1");
|
||||||
|
}
|
||||||
|
desiredOrder.put(assetId, order);
|
||||||
|
}
|
||||||
|
if (desiredOrder.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, Integer> originalIndex = new HashMap<>();
|
||||||
|
for (int index = 0; index < bucket.size(); index++) {
|
||||||
|
originalIndex.put(bucket.get(index).getId(), index);
|
||||||
|
}
|
||||||
|
List<Asset> ordered = new ArrayList<>(bucket);
|
||||||
|
ordered.sort((a, b) -> {
|
||||||
|
int orderA = desiredOrder.getOrDefault(a.getId(), a.getDisplayOrder() != null ? a.getDisplayOrder() : bucket.size() - originalIndex.getOrDefault(a.getId(), bucket.size()));
|
||||||
|
int orderB = desiredOrder.getOrDefault(b.getId(), b.getDisplayOrder() != null ? b.getDisplayOrder() : bucket.size() - originalIndex.getOrDefault(b.getId(), bucket.size()));
|
||||||
|
int cmp = Integer.compare(orderB, orderA);
|
||||||
|
if (cmp != 0) {
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
return Integer.compare(originalIndex.getOrDefault(a.getId(), Integer.MAX_VALUE), originalIndex.getOrDefault(b.getId(), Integer.MAX_VALUE));
|
||||||
|
});
|
||||||
|
List<Asset> changed = new ArrayList<>();
|
||||||
|
for (int index = 0; index < ordered.size(); index++) {
|
||||||
|
Asset asset = ordered.get(index);
|
||||||
|
int nextOrder = ordered.size() - index;
|
||||||
|
if (asset.getDisplayOrder() == null || asset.getDisplayOrder() != nextOrder) {
|
||||||
|
asset.setDisplayOrder(nextOrder);
|
||||||
|
changed.add(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assetRepository.saveAll(changed);
|
||||||
|
publishOrderUpdates(broadcaster, null, changed);
|
||||||
|
for (Asset asset : changed) {
|
||||||
|
if (script) {
|
||||||
|
auditLogService.recordEntry(
|
||||||
|
asset.getBroadcaster(),
|
||||||
|
actor,
|
||||||
|
"SCRIPT_ORDER_UPDATED",
|
||||||
|
formatScriptTransformDetails(asset.getId(), asset.getDisplayOrder())
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
TransformRequest logDetails = new TransformRequest();
|
||||||
|
logDetails.setOrder(asset.getDisplayOrder());
|
||||||
|
auditLogService.recordEntry(
|
||||||
|
asset.getBroadcaster(),
|
||||||
|
actor,
|
||||||
|
"VISUAL_UPDATED",
|
||||||
|
formatVisualTransformDetails(asset.getId(), logDetails)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void validateVisualTransform(TransformRequest req) {
|
private void validateVisualTransform(TransformRequest req) {
|
||||||
Settings settings = settingsService.get();
|
Settings settings = settingsService.get();
|
||||||
double maxSpeed = settings.getMaxAssetPlaybackSpeedFraction();
|
double maxSpeed = settings.getMaxAssetPlaybackSpeedFraction();
|
||||||
@@ -1171,13 +1296,13 @@ public class ChannelDirectoryService {
|
|||||||
int canvasMaxSizePixels = settings.getMaxCanvasSideLengthPixels();
|
int canvasMaxSizePixels = settings.getMaxCanvasSideLengthPixels();
|
||||||
|
|
||||||
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 + "]"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function createAdminConsole({
|
|||||||
const assets = new Map();
|
const assets = new Map();
|
||||||
const mediaCache = new Map();
|
const mediaCache = new Map();
|
||||||
const renderStates = new Map();
|
const renderStates = new Map();
|
||||||
|
const transformBaseline = new Map();
|
||||||
const animatedCache = new Map();
|
const animatedCache = new Map();
|
||||||
const audioControllers = new Map();
|
const audioControllers = new Map();
|
||||||
const pendingAudioUnlock = new Set();
|
const pendingAudioUnlock = new Set();
|
||||||
@@ -506,7 +507,7 @@ export function createAdminConsole({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
updateRenderState(asset);
|
updateRenderState(asset);
|
||||||
schedulePersistTransform(asset);
|
schedulePersistTransform(asset);
|
||||||
drawAndList();
|
drawAndList(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
function connect() {
|
function connect() {
|
||||||
@@ -621,6 +622,7 @@ export function createAdminConsole({
|
|||||||
if (!renderStates.has(asset.id)) {
|
if (!renderStates.has(asset.id)) {
|
||||||
renderStates.set(asset.id, { ...merged });
|
renderStates.set(asset.id, { ...merged });
|
||||||
}
|
}
|
||||||
|
updateTransformBaseline(merged);
|
||||||
resolvePendingUploadByName(asset.name);
|
resolvePendingUploadByName(asset.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -635,6 +637,35 @@ export function createAdminConsole({
|
|||||||
renderStates.set(asset.id, state);
|
renderStates.set(asset.id, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateTransformBaseline(asset) {
|
||||||
|
if (!asset?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const snapshot = {};
|
||||||
|
snapshot.audioVolume = Number.isFinite(asset.audioVolume) ? asset.audioVolume : undefined;
|
||||||
|
if (isAudioAsset(asset)) {
|
||||||
|
snapshot.audioLoop = asset.audioLoop;
|
||||||
|
snapshot.audioDelayMillis = Number.isFinite(asset.audioDelayMillis)
|
||||||
|
? asset.audioDelayMillis
|
||||||
|
: undefined;
|
||||||
|
snapshot.audioSpeed = Number.isFinite(asset.audioSpeed) ? asset.audioSpeed : undefined;
|
||||||
|
snapshot.audioPitch = Number.isFinite(asset.audioPitch) ? asset.audioPitch : undefined;
|
||||||
|
} else {
|
||||||
|
snapshot.x = Number.isFinite(asset.x) ? asset.x : undefined;
|
||||||
|
snapshot.y = Number.isFinite(asset.y) ? asset.y : undefined;
|
||||||
|
snapshot.width = Number.isFinite(asset.width) ? asset.width : undefined;
|
||||||
|
snapshot.height = Number.isFinite(asset.height) ? asset.height : undefined;
|
||||||
|
snapshot.rotation = Number.isFinite(asset.rotation) ? asset.rotation : undefined;
|
||||||
|
snapshot.speed = Number.isFinite(asset.speed) ? asset.speed : undefined;
|
||||||
|
snapshot.muted = asset.muted;
|
||||||
|
const order = isCodeAsset(asset) ? getScriptLayerValue(asset.id) : getLayerValue(asset.id);
|
||||||
|
if (Number.isFinite(order)) {
|
||||||
|
snapshot.order = order;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transformBaseline.set(asset.id, snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
function handleEvent(event) {
|
function handleEvent(event) {
|
||||||
if (event.type === "CANVAS" && event.payload) {
|
if (event.type === "CANVAS" && event.payload) {
|
||||||
applyCanvasSettings(event.payload);
|
applyCanvasSettings(event.payload);
|
||||||
@@ -647,6 +678,7 @@ export function createAdminConsole({
|
|||||||
scriptLayerOrder = scriptLayerOrder.filter((id) => id !== assetId);
|
scriptLayerOrder = scriptLayerOrder.filter((id) => id !== assetId);
|
||||||
clearMedia(assetId);
|
clearMedia(assetId);
|
||||||
renderStates.delete(assetId);
|
renderStates.delete(assetId);
|
||||||
|
transformBaseline.delete(assetId);
|
||||||
loopPlaybackState.delete(assetId);
|
loopPlaybackState.delete(assetId);
|
||||||
cancelPendingTransform(assetId);
|
cancelPendingTransform(assetId);
|
||||||
if (selectedAssetId === assetId) {
|
if (selectedAssetId === assetId) {
|
||||||
@@ -666,7 +698,27 @@ export function createAdminConsole({
|
|||||||
loopPlaybackState.delete(event.payload.id);
|
loopPlaybackState.delete(event.payload.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drawAndList();
|
drawAndList(shouldRenderAssetList(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldRenderAssetList(event) {
|
||||||
|
if (!event) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const { type, payload, patch } = event;
|
||||||
|
if (type === "DELETED" || type === "VISIBILITY") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (payload) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (patch) {
|
||||||
|
if (patch.hidden != null || patch.order != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyPatch(assetId, patch) {
|
function applyPatch(assetId, patch) {
|
||||||
@@ -706,10 +758,12 @@ export function createAdminConsole({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawAndList() {
|
function drawAndList(renderList = true) {
|
||||||
requestDraw();
|
requestDraw();
|
||||||
|
if (renderList) {
|
||||||
renderAssetList();
|
renderAssetList();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function requestDraw() {
|
function requestDraw() {
|
||||||
if (drawPending) {
|
if (drawPending) {
|
||||||
@@ -2055,7 +2109,7 @@ export function createAdminConsole({
|
|||||||
if (media) {
|
if (media) {
|
||||||
applyMediaSettings(media, asset);
|
applyMediaSettings(media, asset);
|
||||||
}
|
}
|
||||||
drawAndList();
|
drawAndList(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateVolumeFromInput() {
|
function updateVolumeFromInput() {
|
||||||
@@ -2074,7 +2128,7 @@ export function createAdminConsole({
|
|||||||
applyAudioSettings(controller, asset);
|
applyAudioSettings(controller, asset);
|
||||||
}
|
}
|
||||||
schedulePersistTransform(asset);
|
schedulePersistTransform(asset);
|
||||||
drawAndList();
|
drawAndList(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAudioSettingsFromInputs() {
|
function updateAudioSettingsFromInputs() {
|
||||||
@@ -2104,7 +2158,7 @@ export function createAdminConsole({
|
|||||||
const controller = ensureAudioController(asset);
|
const controller = ensureAudioController(asset);
|
||||||
applyAudioSettings(controller, asset);
|
applyAudioSettings(controller, asset);
|
||||||
schedulePersistTransform(asset);
|
schedulePersistTransform(asset);
|
||||||
drawAndList();
|
drawAndList(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function nudgeRotation(delta) {
|
function nudgeRotation(delta) {
|
||||||
@@ -2114,7 +2168,7 @@ export function createAdminConsole({
|
|||||||
asset.rotation = next;
|
asset.rotation = next;
|
||||||
updateRenderState(asset);
|
updateRenderState(asset);
|
||||||
persistTransform(asset);
|
persistTransform(asset);
|
||||||
drawAndList();
|
drawAndList(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function recenterSelectedAsset() {
|
function recenterSelectedAsset() {
|
||||||
@@ -2126,7 +2180,7 @@ export function createAdminConsole({
|
|||||||
asset.y = centerY;
|
asset.y = centerY;
|
||||||
updateRenderState(asset);
|
updateRenderState(asset);
|
||||||
persistTransform(asset);
|
persistTransform(asset);
|
||||||
drawAndList();
|
drawAndList(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLayeredAssets(asset) {
|
function getLayeredAssets(asset) {
|
||||||
@@ -2195,20 +2249,90 @@ export function createAdminConsole({
|
|||||||
globalThis.sendToBack = sendToBack;
|
globalThis.sendToBack = sendToBack;
|
||||||
|
|
||||||
function applyLayerOrder(ordered) {
|
function applyLayerOrder(ordered) {
|
||||||
|
if (!ordered || !ordered.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previousOrder = [...layerOrder];
|
||||||
const newOrder = ordered.map((item) => item.id).filter((id) => assets.has(id));
|
const newOrder = ordered.map((item) => item.id).filter((id) => assets.has(id));
|
||||||
layerOrder = newOrder;
|
layerOrder = newOrder;
|
||||||
const changed = ordered.map((item) => assets.get(item.id)).filter(Boolean);
|
const changed = ordered.map((item) => assets.get(item.id)).filter(Boolean);
|
||||||
changed.forEach((item) => updateRenderState(item));
|
changed.forEach((item) => updateRenderState(item));
|
||||||
changed.forEach((item) => schedulePersistTransform(item, true));
|
const orderUpdates = buildOrderUpdates(ordered, previousOrder);
|
||||||
|
sendOrderUpdates(orderUpdates, () => {
|
||||||
|
layerOrder = previousOrder;
|
||||||
|
drawAndList();
|
||||||
|
});
|
||||||
drawAndList();
|
drawAndList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyScriptLayerOrder(ordered) {
|
function applyScriptLayerOrder(ordered) {
|
||||||
|
if (!ordered || !ordered.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previousOrder = [...scriptLayerOrder];
|
||||||
const newOrder = ordered.map((item) => item.id).filter((id) => assets.has(id));
|
const newOrder = ordered.map((item) => item.id).filter((id) => assets.has(id));
|
||||||
scriptLayerOrder = newOrder;
|
scriptLayerOrder = newOrder;
|
||||||
const changed = ordered.map((item) => assets.get(item.id)).filter(Boolean);
|
const orderUpdates = buildOrderUpdates(ordered, previousOrder);
|
||||||
changed.forEach((item) => schedulePersistTransform(item, true));
|
sendOrderUpdates(orderUpdates, () => {
|
||||||
|
scriptLayerOrder = previousOrder;
|
||||||
drawAndList();
|
drawAndList();
|
||||||
|
});
|
||||||
|
drawAndList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOrderUpdates(ordered, previousOrderIds) {
|
||||||
|
if (!ordered || !ordered.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const previousLength = previousOrderIds.length || ordered.length;
|
||||||
|
const previousOrderMap = new Map();
|
||||||
|
previousOrderIds.forEach((id, index) => {
|
||||||
|
previousOrderMap.set(id, previousLength - index);
|
||||||
|
});
|
||||||
|
const updates = [];
|
||||||
|
const newLength = ordered.length;
|
||||||
|
ordered.forEach((asset, index) => {
|
||||||
|
if (!asset) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newOrderValue = newLength - index;
|
||||||
|
const previousValue = previousOrderMap.get(asset.id);
|
||||||
|
if (previousValue !== newOrderValue) {
|
||||||
|
updates.push({ assetId: asset.id, order: newOrderValue });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return updates;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendOrderUpdates(updates, onError) {
|
||||||
|
if (!updates || !updates.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch(`/api/channels/${broadcaster}/assets/order`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ updates }),
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return extractErrorMessage(response, "Unable to reorder assets right now.").then((message) => {
|
||||||
|
throw new Error(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (onError) {
|
||||||
|
onError();
|
||||||
|
}
|
||||||
|
const assetDetails = updates
|
||||||
|
.map((update) => {
|
||||||
|
const asset = assets.get(update.assetId);
|
||||||
|
return `${asset?.name ?? "unknown"} (${update.assetId})`;
|
||||||
|
})
|
||||||
|
.join(", ");
|
||||||
|
console.warn("Asset reorder failed for", assetDetails, error?.message || error);
|
||||||
|
showToast(error?.message || "Unable to reorder assets right now.", "error");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAssetAspectRatio(asset) {
|
function getAssetAspectRatio(asset) {
|
||||||
@@ -2318,6 +2442,7 @@ export function createAdminConsole({
|
|||||||
clearMedia(asset.id);
|
clearMedia(asset.id);
|
||||||
assets.delete(asset.id);
|
assets.delete(asset.id);
|
||||||
renderStates.delete(asset.id);
|
renderStates.delete(asset.id);
|
||||||
|
transformBaseline.delete(asset.id);
|
||||||
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);
|
||||||
@@ -2452,29 +2577,13 @@ export function createAdminConsole({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function persistTransform(asset, silent = false) {
|
function persistTransform(asset, silent = false) {
|
||||||
cancelPendingTransform(asset.id);
|
if (!asset || !asset.id) {
|
||||||
const payload = {
|
return Promise.resolve();
|
||||||
audioLoop: asset.audioLoop,
|
|
||||||
audioDelayMillis: asset.audioDelayMillis,
|
|
||||||
audioSpeed: asset.audioSpeed,
|
|
||||||
audioPitch: asset.audioPitch,
|
|
||||||
audioVolume: asset.audioVolume,
|
|
||||||
};
|
|
||||||
if (isCodeAsset(asset)) {
|
|
||||||
const order = getScriptLayerValue(asset.id);
|
|
||||||
payload.order = order;
|
|
||||||
} else if (!isAudioAsset(asset)) {
|
|
||||||
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.order = order;
|
|
||||||
if (isVideoAsset(asset)) {
|
|
||||||
payload.muted = asset.muted;
|
|
||||||
}
|
}
|
||||||
|
cancelPendingTransform(asset.id);
|
||||||
|
const payload = buildTransformPayload(asset);
|
||||||
|
if (!Object.keys(payload).length) {
|
||||||
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
fetch(`/api/channels/${broadcaster}/assets/${asset.id}/transform`, {
|
fetch(`/api/channels/${broadcaster}/assets/${asset.id}/transform`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
@@ -2493,7 +2602,7 @@ export function createAdminConsole({
|
|||||||
storeAsset(updated);
|
storeAsset(updated);
|
||||||
updateRenderState(updated);
|
updateRenderState(updated);
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
drawAndList();
|
drawAndList(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -2503,6 +2612,57 @@ export function createAdminConsole({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildTransformPayload(asset) {
|
||||||
|
if (!asset?.id) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const baseline = transformBaseline.get(asset.id) || {};
|
||||||
|
const payload = {};
|
||||||
|
const addNumber = (key, value) => {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previous = baseline[key];
|
||||||
|
if (Number.isFinite(previous) && previous === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload[key] = value;
|
||||||
|
};
|
||||||
|
const addBoolean = (key, value) => {
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (baseline[key] === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload[key] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
addBoolean("audioLoop", asset.audioLoop);
|
||||||
|
addNumber("audioDelayMillis", asset.audioDelayMillis);
|
||||||
|
addNumber("audioSpeed", asset.audioSpeed);
|
||||||
|
addNumber("audioPitch", asset.audioPitch);
|
||||||
|
addNumber("audioVolume", asset.audioVolume);
|
||||||
|
|
||||||
|
if (!isAudioAsset(asset)) {
|
||||||
|
addNumber("x", asset.x);
|
||||||
|
addNumber("y", asset.y);
|
||||||
|
addNumber("width", asset.width);
|
||||||
|
addNumber("height", asset.height);
|
||||||
|
addNumber("rotation", asset.rotation);
|
||||||
|
addNumber("speed", asset.speed);
|
||||||
|
if (isVideoAsset(asset)) {
|
||||||
|
addBoolean("muted", asset.muted);
|
||||||
|
}
|
||||||
|
const order = isCodeAsset(asset) ? getScriptLayerValue(asset.id) : getLayerValue(asset.id);
|
||||||
|
if (Number.isFinite(order) && baseline.order !== order) {
|
||||||
|
payload.order = order;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
canvas.addEventListener("mousedown", (event) => {
|
canvas.addEventListener("mousedown", (event) => {
|
||||||
const point = getCanvasPoint(event);
|
const point = getCanvasPoint(event);
|
||||||
const current = getSelectedAsset();
|
const current = getSelectedAsset();
|
||||||
|
|||||||
@@ -10,24 +10,18 @@ export function getVisibilityState(state, asset) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function smoothState(state, asset) {
|
export function smoothState(state, asset) {
|
||||||
const previous = state.renderStates.get(asset.id) || { ...asset };
|
const previous = state.renderStates.get(asset.id) || {};
|
||||||
const factor = 0.15;
|
|
||||||
const next = {
|
const next = {
|
||||||
x: lerp(previous.x, asset.x, factor),
|
x: Number.isFinite(asset.x) ? asset.x : previous.x ?? 0,
|
||||||
y: lerp(previous.y, asset.y, factor),
|
y: Number.isFinite(asset.y) ? asset.y : previous.y ?? 0,
|
||||||
width: lerp(previous.width, asset.width, factor),
|
width: Number.isFinite(asset.width) ? asset.width : previous.width ?? 0,
|
||||||
height: lerp(previous.height, asset.height, factor),
|
height: Number.isFinite(asset.height) ? asset.height : previous.height ?? 0,
|
||||||
rotation: smoothAngle(previous.rotation, asset.rotation, factor),
|
rotation: Number.isFinite(asset.rotation) ? asset.rotation : previous.rotation ?? 0,
|
||||||
};
|
};
|
||||||
state.renderStates.set(asset.id, next);
|
state.renderStates.set(asset.id, next);
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
function smoothAngle(current, target, factor) {
|
|
||||||
const delta = ((target - current + 180) % 360) - 180;
|
|
||||||
return current + delta * factor;
|
|
||||||
}
|
|
||||||
|
|
||||||
function lerp(a, b, t) {
|
function lerp(a, b, t) {
|
||||||
return a + (b - a) * t;
|
return a + (b - a) * t;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user