nitro-v36-with-renderer-v36
/
src /
components /
room /
widgets /
avatar-info /
infostand /
InfoStandWidgetFurniView.tsx
InfoStandWidgetFurniView.tsx 73.1 Ko
Télécharger le fichier
import {
CrackableDataType,
CreateLinkEvent,
FurnitureFloorUpdateEvent,
GetRoomEngine,
GetSessionDataManager,
GetSoundManager,
GroupInformationComposer,
GroupInformationEvent,
NowPlayingEvent,
RoomControllerLevel,
RoomObjectCategory,
RoomObjectOperationType,
RoomObjectVariable,
RoomRemoveBackgroundComposer,
RoomWidgetEnumItemExtradataParameter,
RoomWidgetFurniInfoUsagePolicyEnum,
SetObjectDataMessageComposer,
SongInfoReceivedEvent,
UpdateFurniturePositionComposer
} from '@octane/renderer';
import { FC, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FaEraser, FaTimes } from 'react-icons/fa';
import { GrFormNextLink, GrRotateLeft, GrRotateRight } from 'react-icons/gr';
import {
AvatarInfoFurni,
CopyToClipboard,
FriendlyTime,
GetConfigurationValue,
GetGroupInformation,
getWiredChestInfostandData,
IPhotoData,
isRecolorableClass,
recolorOfData,
isSafeExternalUrl,
LocalizeText,
localizeWithFallback,
SendMessageComposer
} from '../../../../../api';
import {
Button,
Column,
Flex,
LayoutBadgeImageView,
LayoutCurrencyIcon,
LayoutLimitedEditionCompactPlateView,
LayoutRarityLevelView,
LayoutRoomObjectImageView,
PIXEL_ART_RENDERING,
Text,
UserProfileIconView
} from '../../../../../common';
import {
useFurniPickupGuard,
useFurnitureRecolorWidget,
useHasPermission,
useMessageEvent,
useOctaneEvent,
useRareValues,
useRecolorableColorStore,
useRoom,
useWiredLabelStore,
useWiredTools
} from '../../../../../hooks';
import { OctaneInput } from '../../../../../layout';
import statHeightIcon from '../../../../../assets/images/infostand/stat-height.png';
import statIdIcon from '../../../../../assets/images/infostand/stat-id.png';
import statRotationIcon from '../../../../../assets/images/infostand/stat-rotation.png';
import statStateIcon from '../../../../../assets/images/infostand/stat-state.png';
import statXIcon from '../../../../../assets/images/infostand/stat-x.png';
import statYIcon from '../../../../../assets/images/infostand/stat-y.png';
import chestLockedIcon from '../../../../../assets/images/groups/native/forum_forum_locked.png';
import { ImagePositionEditorView } from './ImagePositionEditorView';
import { BRANDING_NUMBER_RANGES, brandingSaveMap, infostandFurniOwnerText, infostandFurniPanelClass, parseBrandingOptions, parseFurniHeight } from './infostandFurni.helpers';
import { BaseButton } from '../../../../../common/BaseButton';
import { NumberInput } from '../../../../../common/NumberInput';
import { Hint } from '../../../../../common/Tooltip';
import { NativeTextTree } from '../../../../../common/native-text/NativeTextTree';
interface InfoStandWidgetFurniViewProps {
avatarInfo: AvatarInfoFurni;
onClose: () => void;
}
const PICKUP_MODE_NONE: number = 0;
const PICKUP_MODE_EJECT: number = 1;
const PICKUP_MODE_FULL: number = 2;
const removeLandscapeLabel = () => {
const localized = LocalizeText('infostand.button.remove_landscape');
return !localized || localized === 'infostand.button.remove_landscape' ? 'Remove Landscape' : localized;
};
// One cell of the infostand stats grid: an icon and a value. Clicking copies the
// value (a check replaces it for a moment); the cell lights up under the mouse.
const InfoStandStat: FC<{ icon: ReactNode; label: string; value: string | number; iconAfter?: boolean; tooltip?: string }> = ({
icon,
label,
value,
iconAfter = false,
tooltip
}) => {
const [copied, setCopied] = useState(false);
useEffect(() => {
if (!copied) return;
const timeout = setTimeout(() => setCopied(false), 1200);
return () => clearTimeout(timeout);
}, [copied]);
const shown = copied ? '✓' : value;
return (
<NativeTextTree>
<Hint content={`${tooltip ?? label}: ${value} - ${localizeWithFallback('infostand.copy.tooltip', 'Click to copy')}`}>
<BaseButton
type="button"
className={`group flex items-center gap-1 min-w-0 w-full rounded px-0.5 border-0 bg-transparent cursor-pointer transition-colors duration-100 hover:bg-[#ffffff14] active:bg-[#ffffff24] focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#7ec8e3] ${iconAfter ? 'justify-end' : 'justify-start'}`}
onClick={() => void CopyToClipboard(String(value)).then((ok) => setCopied(ok))}
>
{!iconAfter && icon}
<Text small textBreak variant="white" className={`transition-colors duration-100 ${copied ? '!text-[#7ec8e3]' : 'group-hover:!text-[#7ec8e3]'}`}>
{iconAfter ? `${shown}:` : `:${shown}`}
</Text>
{iconAfter && icon}
</BaseButton>
</Hint>
</NativeTextTree>
);
};
const StatIcon: FC<{ src: string }> = ({ src }) => (
<NativeTextTree>
<img alt="" className="shrink-0 w-[22px] h-[22px] -my-[1px] transition-[filter] duration-100 group-hover:brightness-150 group-active:brightness-125" draggable={false} src={src} style={{ imageRendering: PIXEL_ART_RENDERING }} />
</NativeTextTree>
);
function formatPlantDuration(totalSeconds: number): string {
const seconds = Math.max(0, Math.floor(totalSeconds));
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) return `${hours}h ${minutes}m`;
if (minutes > 0) return `${minutes}m ${secs}s`;
return `${secs}s`;
}
function getValidRoomObjectDirection(roomObject: any, isPositive: boolean) {
if (!roomObject || !roomObject.model) return 0;
let allowedDirections: number[] = [];
if (roomObject.type === 'monster_plant') {
allowedDirections = roomObject.model.getValue('pet_allowed_directions');
} else {
allowedDirections = roomObject.model.getValue('furniture_allowed_directions');
}
let direction = roomObject.getDirection().x;
if (allowedDirections && allowedDirections.length) {
let index = allowedDirections.indexOf(direction);
if (index < 0) {
index = 0;
for (let i = 0; i < allowedDirections.length; i++) {
if (direction <= allowedDirections[i]) break;
index++;
}
index = index % allowedDirections.length;
}
if (isPositive) {
index = (index + 1) % allowedDirections.length;
} else {
index = (index - 1 + allowedDirections.length) % allowedDirections.length;
}
direction = allowedDirections[index];
}
return direction;
}
export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = (props) => {
const { avatarInfo = null, onClose = null } = props;
const { roomSession = null } = useRoom();
const { openInspectionForFurni, showInspectButton, roomSettings = null } = useWiredTools();
const hasLabel = useWiredLabelStore((state) => !!avatarInfo && !avatarInfo.isWallItem && !!state.labels[avatarInfo.id]);
const openLabelSettings = useWiredLabelStore((state) => state.openSettings);
const isModerator = useHasPermission('acc_anyroomowner');
const { getValue: getRareValue } = useRareValues();
const rareValue = useMemo(() => (avatarInfo ? getRareValue(avatarInfo.spriteId) : null), [avatarInfo, getRareValue]);
const externalImagePhotoUrl = useMemo(() => {
if (!avatarInfo || !roomSession) return null;
const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.category);
if (!roomObject || roomObject.type.indexOf('external_image') !== 0) return null;
try {
const photoData = JSON.parse(roomObject.model.getValue<string>(RoomObjectVariable.FURNITURE_DATA)) as IPhotoData;
const photoUrl = photoData?.w ?? null;
return photoUrl && isSafeExternalUrl(photoUrl) ? photoUrl : null;
} catch {
return null;
}
}, [avatarInfo, roomSession]);
const descriptionsEnabled = GetConfigurationValue<boolean>('furni.descriptions.enabled', true);
const itemLocationEnabled = GetConfigurationValue<boolean>('furni.location.enabled', true);
const itemLocationRequireAccess = GetConfigurationValue<boolean>('furni.location.require.access', true);
const [pickupMode, setPickupMode] = useState(0);
const [canMove, setCanMove] = useState(false);
const [canRotate, setCanRotate] = useState(false);
const [canRecolor, setCanRecolor] = useState(false);
const { open: openRecolor = null } = useFurnitureRecolorWidget();
const [canUse, setCanUse] = useState(false);
const { pickupRoomObject } = useFurniPickupGuard();
const [canRemoveBackground, setCanRemoveBackground] = useState(false);
const [furniKeys, setFurniKeys] = useState<string[]>([]);
const [furniValues, setFurniValues] = useState<string[]>([]);
const [customKeys, setCustomKeys] = useState<string[]>([]);
const [customValues, setCustomValues] = useState<string[]>([]);
const [isCrackable, setIsCrackable] = useState(false);
const [crackableHits, setCrackableHits] = useState(0);
const [crackableTarget, setCrackableTarget] = useState(0);
const [isPlant, setIsPlant] = useState(false);
const [plantWaterSeconds, setPlantWaterSeconds] = useState(0);
const [plantDeathSeconds, setPlantDeathSeconds] = useState(0);
const [plantDead, setPlantDead] = useState(false);
const plantClassnames = GetConfigurationValue<string[]>('furni.plant.classnames', []) ?? [];
const [godMode, setGodMode] = useState(false);
const [canSeeFurniId, setCanSeeFurniId] = useState(false);
// Official InfoStandFurniView: the group name and badge both come from the group details, not from the stuff data.
const [groupName, setGroupName] = useState<string>(null);
const [groupBadge, setGroupBadge] = useState<string>(null);
const [isJukeBox, setIsJukeBox] = useState<boolean>(false);
const [isSongDisk, setIsSongDisk] = useState<boolean>(false);
const [isBranded, setIsBranded] = useState<boolean>(false);
const [showPositionEditor, setShowPositionEditor] = useState<boolean>(false);
const [songId, setSongId] = useState<number>(-1);
const [songName, setSongName] = useState<string>('');
const [songCreator, setSongCreator] = useState<string>('');
const [itemLocation, setItemLocation] = useState<{ x: number; y: number; z: number }>({ x: -1, y: -1, z: -1 });
const [dropdownOpen, setDropdownOpen] = useState(sessionStorage.getItem('dropdownOpen') === 'true');
const [furniLocationZ, setFurniLocationZ] = useState<number>(null);
const [furniDirection, setFurniDirection] = useState(0);
const [furniState, setFurniState] = useState(0);
// The room object moves, turns and changes state without a message this view
// listens to (drag in the room, use, wired), so follow it while it is selected.
// A reading is shown only once it held still for two polls: a drag or a wired
// slide passes through many positions and only the final one is worth showing.
useEffect(() => {
if (!avatarInfo || !roomSession) return;
const category = avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR;
let candidate = '';
let held = 0;
const sync = (immediate: boolean) => {
const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, category);
if (!roomObject) return;
const location = roomObject.getLocation();
const direction = Math.round((roomObject.getDirection()?.x ?? 0) / 45);
const state = Number(roomObject.getState(0) ?? 0);
const reading = `${location.x}|${location.y}|${location.z}|${direction}|${state}`;
held = reading === candidate ? held + 1 : 1;
candidate = reading;
if (!immediate && held < 2) return;
setItemLocation((prev) => (prev.x === location.x && prev.y === location.y && prev.z === location.z ? prev : { x: location.x, y: location.y, z: location.z }));
setFurniDirection(direction);
setFurniState(state);
};
sync(true);
const interval = setInterval(() => sync(false), 150);
return () => clearInterval(interval);
}, [avatarInfo, roomSession]);
const showOwnerProfileIcon = useMemo(() => {
// A temporary (Wired) copy and a Builders Club furni show no owner: no profile link (official setOwnerInfo).
return !!avatarInfo && !avatarInfo.isTemporary && !avatarInfo.isBuildersClub && avatarInfo.ownerId > 0;
}, [avatarInfo]);
const sendUpdate = useCallback(
(deltaX: number, deltaY: number, newZ: number = 0, deltaDirection: number = 0) => {
if (!avatarInfo) return;
const roomId = GetRoomEngine().activeRoomId;
const roomObject = GetRoomEngine().getRoomObject(roomId, avatarInfo.id, avatarInfo.category);
if (!roomObject) return;
const newX = roomObject.getLocation().x + deltaX;
const newY = roomObject.getLocation().y + deltaY;
const currentDirection = roomObject.getDirection().x;
const newDirection = deltaDirection !== 0 ? getValidRoomObjectDirection(roomObject, deltaDirection > 0) / 45 : currentDirection / 45;
SendMessageComposer(new UpdateFurniturePositionComposer(avatarInfo.id, newX, newY, Math.round(newZ * 10000), newDirection));
},
[avatarInfo]
);
// Typing "12.5" used to move the furni at "1", "12", "12." and "12.5": the height now goes out once the
// field has rested, and at once when it loses focus.
const heightSendTimer = useRef<ReturnType<typeof setTimeout>>(null);
useEffect(() => () => clearTimeout(heightSendTimer.current), []);
const handleHeightChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const newZ = parseFurniHeight(event.target.value);
clearTimeout(heightSendTimer.current);
// An empty field is someone typing, not a move to the floor.
if (newZ === null) {
setFurniLocationZ(null);
return;
}
setFurniLocationZ(newZ);
heightSendTimer.current = setTimeout(() => sendUpdate(0, 0, newZ, 0), 300);
},
[sendUpdate]
);
const handleHeightBlur = useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
clearTimeout(heightSendTimer.current);
const newZ = parseFurniHeight(event.target.value);
// Left empty: the field shows where the furni really is again, and nothing moves.
if (newZ === null) {
const roomObject = avatarInfo ? GetRoomEngine().getRoomObject(GetRoomEngine().activeRoomId, avatarInfo.id, avatarInfo.category) : null;
setFurniLocationZ(roomObject ? roomObject.getLocation().z : null);
return;
}
setFurniLocationZ(newZ);
sendUpdate(0, 0, newZ, 0);
},
[avatarInfo, sendUpdate]
);
const adjustHeight = useCallback(
(amount: number) => {
let newZ = (furniLocationZ ?? 0) + amount;
if (newZ < 0) newZ = 0;
else if (newZ > 40) newZ = 40;
newZ = parseFloat(newZ.toFixed(4));
setFurniLocationZ(newZ);
sendUpdate(0, 0, newZ, 0);
},
[furniLocationZ, sendUpdate]
);
useOctaneEvent<NowPlayingEvent>(
NowPlayingEvent.NPE_SONG_CHANGED,
(event) => {
setSongId(event.id);
},
isJukeBox || isSongDisk
);
useOctaneEvent<NowPlayingEvent>(
SongInfoReceivedEvent.SIR_TRAX_SONG_INFO_RECEIVED,
(event) => {
if (event.id !== songId) return;
const songInfo = GetSoundManager().musicController.getSongInfo(event.id);
if (!songInfo) return;
setSongName(songInfo.name);
setSongCreator(songInfo.creator);
},
isJukeBox || isSongDisk
);
useEffect(() => {
let pickupMode = PICKUP_MODE_NONE;
let canMove = false;
let canRotate = false;
let canUse = false;
let furniKeyss: string[] = [];
let furniValuess: string[] = [];
let customKeyss: string[] = [];
let customValuess: string[] = [];
let isCrackable = false;
let crackableHits = 0;
let crackableTarget = 0;
let isPlant = false;
let plantWaterSeconds = 0;
let plantDeathSeconds = 0;
let plantDead = false;
let godMode = false;
let canSeeFurniId = false;
let furniIsJukebox = false;
let furniIsSongDisk = false;
let furniSongId = -1;
const roomObjForLocation = GetRoomEngine().getRoomObject(
roomSession.roomId,
avatarInfo.id,
avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR
);
const location = roomObjForLocation?.getLocation();
if (location) {
setItemLocation({ x: location.x, y: location.y, z: location.z });
setFurniLocationZ(location.z);
}
const isValidController = avatarInfo.roomControllerLevel >= RoomControllerLevel.GUEST;
// AIR InfoStandFurniView: playtest mode (wired menu preference) hides move, rotate, pick up and
// the rights-only use for owners and staff too; the free furni movements mode below still shows them.
const isPlayTest = !!roomSession?.playTestMode;
let removeBackgroundAllowed = false;
if (!isPlayTest && (isValidController || avatarInfo.isOwner || avatarInfo.isRoomOwner || avatarInfo.isAnyRoomController)) {
canMove = true;
canRotate = !avatarInfo.isWallItem;
if (avatarInfo.roomControllerLevel >= RoomControllerLevel.MODERATOR) godMode = true;
if (avatarInfo.isWallItem) {
const maskType = roomObjForLocation?.model?.getValue<string>(RoomObjectVariable.FURNITURE_PLANE_MASK_TYPE);
if (maskType && maskType.length) removeBackgroundAllowed = true;
}
}
if (avatarInfo.isAnyRoomController) {
canSeeFurniId = true;
}
if (
avatarInfo.usagePolicy === RoomWidgetFurniInfoUsagePolicyEnum.EVERYBODY ||
(!isPlayTest &&
((avatarInfo.usagePolicy === RoomWidgetFurniInfoUsagePolicyEnum.CONTROLLER && isValidController) ||
(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX && isValidController) ||
(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.USABLE_PRODUCT && isValidController)))
)
canUse = true;
// AIR InfoStandFurniView: the room's free furni movements mode (ConfigurationItemStates) shows move,
// rotate (floor only) and use for any furni. Recolour keeps following real rights.
const canRecolorByRights = canMove;
if (GetRoomEngine().activeRoomHasFreeFurniMovementsMode) {
canMove = true;
canRotate = !avatarInfo.isWallItem;
canUse = true;
}
if (avatarInfo.extraParam) {
if (avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.CRACKABLE_FURNI) {
const stuffData = avatarInfo.stuffData as CrackableDataType;
canUse = true;
if (roomObjForLocation && plantClassnames.indexOf(roomObjForLocation.type) >= 0) {
isPlant = true;
const rawDeath = stuffData?.target ?? 0;
plantDead = rawDeath < 0;
plantWaterSeconds = Math.max(0, stuffData?.hits ?? 0);
plantDeathSeconds = plantDead ? 0 : rawDeath;
} else {
isCrackable = true;
crackableHits = stuffData?.hits ?? 0;
crackableTarget = stuffData?.target ?? 0;
}
} else if (avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX) {
const playlist = GetSoundManager().musicController.getRoomItemPlaylist();
if (playlist) {
furniSongId = playlist.currentSongId;
}
furniIsJukebox = true;
} else if (avatarInfo.extraParam.indexOf(RoomWidgetEnumItemExtradataParameter.SONGDISK) === 0) {
furniSongId = parseInt(avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.SONGDISK.length));
furniIsSongDisk = true;
}
if (godMode) {
const extraParam = avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.BRANDING_OPTIONS.length);
if (extraParam) {
const { keys, values } = parseBrandingOptions(extraParam);
furniKeyss.push(...keys);
furniValuess.push(...values);
}
}
}
if (godMode) {
const roomObject = GetRoomEngine().getRoomObject(
roomSession.roomId,
avatarInfo.id,
avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR
);
if (roomObject) {
const customVariables = roomObject.model.getValue<string[]>(RoomObjectVariable.FURNITURE_CUSTOM_VARIABLES);
const furnitureData = roomObject.model.getValue<{ [index: string]: string }>(RoomObjectVariable.FURNITURE_DATA);
if (customVariables && customVariables.length) {
for (const customVariable of customVariables) {
customKeyss.push(customVariable);
customValuess.push(furnitureData[customVariable] || '');
}
}
}
}
if (avatarInfo.isOwner || avatarInfo.isAnyRoomController) pickupMode = PICKUP_MODE_FULL;
else if (avatarInfo.isRoomOwner || avatarInfo.roomControllerLevel >= RoomControllerLevel.GUILD_ADMIN) pickupMode = PICKUP_MODE_EJECT;
if (avatarInfo.isStickie || isPlayTest) pickupMode = PICKUP_MODE_NONE;
setPickupMode(pickupMode);
setCanMove(canMove);
setCanRotate(canRotate);
const recolorTypeId = avatarInfo.isWallItem ? 0 : (roomObjForLocation?.model?.getValue<number>(RoomObjectVariable.FURNITURE_TYPE_ID) ?? 0);
const recolorData = recolorTypeId > 0 ? GetSessionDataManager().getFloorItemData(recolorTypeId) : null;
// Only placed recolourable furni (*_recolorable) can be recoloured; the old Builders Club colour
// variants (bc_x*N) keep the colour they were placed in.
setCanRecolor(canRecolorByRights && isRecolorableClass(recolorData?.className));
setCanUse(canUse);
setCanRemoveBackground(removeBackgroundAllowed);
setFurniKeys(furniKeyss);
setFurniValues(furniValuess);
setCustomKeys(customKeyss);
setCustomValues(customValuess);
setIsCrackable(isCrackable);
setCrackableHits(crackableHits);
setCrackableTarget(crackableTarget);
setIsPlant(isPlant);
setPlantWaterSeconds(plantWaterSeconds);
setPlantDeathSeconds(plantDeathSeconds);
setPlantDead(plantDead);
setGodMode(godMode);
setCanSeeFurniId(canSeeFurniId);
setGroupName(null);
setGroupBadge(null);
setIsJukeBox(furniIsJukebox);
setIsSongDisk(furniIsSongDisk);
setSongId(furniSongId);
setIsBranded(!!avatarInfo.extraParam && avatarInfo.extraParam.indexOf(RoomWidgetEnumItemExtradataParameter.BRANDING_OPTIONS) === 0);
if (avatarInfo.groupId) SendMessageComposer(new GroupInformationComposer(avatarInfo.groupId, false));
}, [roomSession, avatarInfo]);
useMessageEvent<GroupInformationEvent>(GroupInformationEvent, (event) => {
const parser = event.getParser();
// Official onGroupDetails: any details of the furni's group fill its badge and name, also the ones a click asked for.
if (!avatarInfo || avatarInfo.groupId !== parser.id) return;
setGroupBadge(parser.badge);
setGroupName(parser.title);
});
useMessageEvent<FurnitureFloorUpdateEvent>(FurnitureFloorUpdateEvent, (event) => {
const parser = event.getParser();
const item = parser.item;
if (!avatarInfo || item.itemId !== avatarInfo.id) return;
setItemLocation({ x: item.x, y: item.y, z: item.z });
setFurniLocationZ(item.z);
if (isPlant) {
const data = item.data as CrackableDataType;
if (data) {
const rawDeath = data.target ?? 0;
const dead = rawDeath < 0;
setPlantDead(dead);
setPlantWaterSeconds(Math.max(0, data.hits ?? 0));
setPlantDeathSeconds(dead ? 0 : rawDeath);
}
}
});
useEffect(() => {
const songInfo = GetSoundManager().musicController.getSongInfo(songId);
setSongName(songInfo?.name ?? '');
setSongCreator(songInfo?.creator ?? '');
}, [songId]);
useEffect(() => {
if (!isPlant || plantDead) return;
const handle = setInterval(() => {
setPlantWaterSeconds((seconds) => (seconds > 0 ? seconds - 1 : 0));
setPlantDeathSeconds((seconds) => (seconds > 0 ? seconds - 1 : 0));
}, 1000);
return () => clearInterval(handle);
}, [isPlant, plantDead]);
const onFurniSettingChange = useCallback(
(index: number, value: string) => {
const clone = Array.from(furniValues);
clone[index] = value;
setFurniValues(clone);
},
[furniValues]
);
const onCustomVariableChange = useCallback(
(index: number, value: string) => {
const clone = Array.from(customValues);
clone[index] = value;
setCustomValues(clone);
},
[customValues]
);
const getBrandingOffset = useCallback(
(key: string): number => {
const index = furniKeys.indexOf(key);
if (index < 0) return 0;
const value = parseInt(furniValues[index]);
return isNaN(value) ? 0 : value;
},
[furniKeys, furniValues]
);
const hasBrandingOffsets = isBranded && furniKeys.indexOf('offsetX') >= 0;
const savePositionEditor = useCallback(
(x: number, y: number, z: number, scale: number, alpha: number) => {
const map = new Map<string, string>();
const clone = Array.from(furniValues);
let hasScale = false;
let hasAlpha = false;
for (let i = 0; i < furniKeys.length; i++) {
const key = furniKeys[i];
let value = furniValues[i];
if (key === 'offsetX') value = String(x);
else if (key === 'offsetY') value = String(y);
else if (key === 'offsetZ') value = String(z);
else if (key === 'scale') {
value = String(scale);
hasScale = true;
} else if (key === 'alpha') {
value = String(alpha);
hasAlpha = true;
}
clone[i] = value;
map.set(key, value);
}
if (!hasScale) map.set('scale', String(scale));
if (!hasAlpha) map.set('alpha', String(alpha));
GetRoomEngine().modifyRoomObjectDataWithMap(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_SAVE_STUFF_DATA, map);
setFurniValues(clone);
},
[avatarInfo, furniKeys, furniValues]
);
const processButtonAction = useCallback(
(action: string) => {
if (!action || action === '') return;
let objectData: string = null;
switch (action) {
case 'buy_one': {
// One more of a recolourable furni comes in the colour of this one.
if (!avatarInfo.isWallItem) {
const roomObject = GetRoomEngine().getRoomObject(GetRoomEngine().activeRoomId, avatarInfo.id, avatarInfo.category);
const typeId = roomObject?.model?.getValue<number>(RoomObjectVariable.FURNITURE_TYPE_ID) ?? 0;
const className = typeId > 0 ? GetSessionDataManager().getFloorItemData(typeId)?.className : null;
if (isRecolorableClass(className)) {
useRecolorableColorStore
.getState()
.setColor(className, recolorOfData(roomObject.model.getValue<{ [key: string]: string }>(RoomObjectVariable.FURNITURE_DATA)));
}
}
CreateLinkEvent(`catalog/open/offerId/${avatarInfo.purchaseOfferId}`);
return;
}
case 'move':
GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_MOVE);
break;
case 'rotate':
GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_ROTATE_POSITIVE);
break;
case 'pickup':
pickupRoomObject(
avatarInfo.id,
avatarInfo.category,
pickupMode === PICKUP_MODE_FULL ? RoomObjectOperationType.OBJECT_PICKUP : RoomObjectOperationType.OBJECT_EJECT
);
break;
case 'use':
GetRoomEngine().useRoomObject(avatarInfo.id, avatarInfo.category);
break;
case 'remove_background':
SendMessageComposer(new RoomRemoveBackgroundComposer());
break;
case 'save_branding_configuration': {
const mapData = brandingSaveMap(furniKeys, furniValues);
GetRoomEngine().modifyRoomObjectDataWithMap(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_SAVE_STUFF_DATA, mapData);
break;
}
case 'save_custom_variables': {
const map = new Map();
for (let i = 0; i < customKeys.length; i++) {
const key = customKeys[i];
const value = customValues[i];
if (key && key.length && value && value.length) map.set(key, value);
}
SendMessageComposer(new SetObjectDataMessageComposer(avatarInfo.id, map));
break;
}
}
},
[avatarInfo, pickupMode, customKeys, customValues, furniKeys, furniValues, pickupRoomObject]
);
if (!avatarInfo) return null;
const furniTypeId = roomSession
? (GetRoomEngine()
.getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR)
?.model?.getValue<number>(RoomObjectVariable.FURNITURE_TYPE_ID) ?? '?')
: '?';
const showLocation = itemLocation.x > -1 && itemLocationEnabled && (!itemLocationRequireAccess || canMove);
const showIds = godMode && canSeeFurniId;
const showEditFurni = godMode && isModerator;
const showBuildtools = godMode && !avatarInfo.isWallItem && canMove;
// AIR InfoStandFurniView.showChestData: a furni / coins chest (furnidata special type 24 / 25) shows its name,
// its contents count and, with wired enabled, the wired (and locked) icons.
const chestData = avatarInfo.isWallItem
? null
: getWiredChestInfostandData(GetSessionDataManager().getFloorItemData(avatarInfo.spriteId)?.specialType ?? -1, avatarInfo.stuffData);
return (
<NativeTextTree>
<Column alignItems="end" gap={1}>
<Column
className={
'relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded ' +
// Official colours (InfoStandFurniView.update): Builders Club 0x331C00, temporary (Wired) 0x142B44, else dark.
infostandFurniPanelClass(avatarInfo)
}
>
<Column className="h-full p-[8px] overflow-auto" gap={1} overflow="visible">
<div className="flex flex-col gap-1">
<Flex alignItems="center" gap={1} justifyContent="between">
<Text small wrap className="octane-infostand__name" variant="white">
{avatarInfo.name}
</Text>
<FaTimes className="cursor-pointer fa-icon" onClick={onClose} />
</Flex>
{!!chestData?.chestName && (
<Text small wrap className="octane-infostand-chest-name" variant="white">
{chestData.chestName}
</Text>
)}
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
</div>
{!isBranded && (
<div className="flex flex-col gap-1">
<Flex gap={1} position="relative">
{avatarInfo.stuffData.isUnique && (
<div className="absolute inset-e-0">
<LayoutLimitedEditionCompactPlateView
uniqueNumber={avatarInfo.stuffData.uniqueNumber}
uniqueSeries={avatarInfo.stuffData.uniqueSeries}
/>
</div>
)}
{avatarInfo.stuffData.rarityLevel > -1 && (
<div className="absolute inset-e-0">
<LayoutRarityLevelView level={avatarInfo.stuffData.rarityLevel} />
</div>
)}
{chestData?.wiredEnabled && (
<div className="octane-infostand-chest-wired absolute inset-e-0 top-0 z-[1] flex items-start gap-[3px]">
{chestData.locked && <img alt="" className="[image-rendering:pixelated]" draggable={false} src={chestLockedIcon} />}
<img
alt=""
className="[image-rendering:pixelated]"
draggable={false}
src={GetConfigurationValue<string>('catalog.asset.icon.url', '').replace('%name%', '80')}
/>
</div>
)}
<Flex center fullWidth className="min-h-[74px] max-h-[86px] overflow-hidden">
{externalImagePhotoUrl ? (
<div className="bg-white p-[3px] pb-[9px] rounded-[2px] border border-[#00000080] shadow-[1px_2px_3px_rgba(0,0,0,0.5)]">
<img
alt=""
draggable={false}
src={externalImagePhotoUrl}
style={{ width: 64, height: 64, objectFit: 'contain', imageRendering: PIXEL_ART_RENDERING }}
/>
</div>
) : (
<LayoutRoomObjectImageView
category={avatarInfo.category}
objectId={avatarInfo.id}
roomId={roomSession.roomId}
style={{
maxWidth: 120,
maxHeight: 82,
backgroundSize: 'contain',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
imageRendering: 'auto'
}}
/>
)}
</Flex>
</Flex>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
</div>
)}
{avatarInfo.description && descriptionsEnabled && (
<Column gap={1}>
<Text fullWidth wrap textBreak variant="white" small>
{avatarInfo.description}
</Text>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
</Column>
)}
{chestData && (
<Column gap={1}>
<Text fullWidth wrap small className="octane-infostand-chest-count" variant="white">
{chestData.countText}
</Text>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
</Column>
)}
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
{showOwnerProfileIcon && <UserProfileIconView userId={avatarInfo.ownerId} />}
{avatarInfo.isTemporary && (
<img
alt=""
className="[image-rendering:pixelated]"
draggable={false}
src={GetConfigurationValue<string>('catalog.asset.icon.url', '').replace('%name%', '80')}
/>
)}
{avatarInfo.isBuildersClub && !avatarInfo.isTemporary && <i className="octane-icon icon-buildersclub shrink-0 scale-75" />}
<Text small wrap variant="white">
{infostandFurniOwnerText(avatarInfo)}
</Text>
</div>
{avatarInfo.purchaseOfferId > 0 && (
<Flex>
<Text pointer small underline variant="white" onClick={(event) => processButtonAction('buy_one')}>
{LocalizeText('infostand.button.buy')}
</Text>
</Flex>
)}
{avatarInfo.isOwner && avatarInfo.expiration >= 0 && (
<Text small wrap variant="white">
{localizeWithFallback('infostand.rent.expiration', `Rental time remaining: ${FriendlyTime.format(avatarInfo.expiration)}`, ['time', 'TIME'], [FriendlyTime.format(avatarInfo.expiration), FriendlyTime.format(avatarInfo.expiration)])}
</Text>
)}
{avatarInfo.isOwner && avatarInfo.expiration >= 0 && (avatarInfo.rentCouldBeUsedForBuyout || avatarInfo.purchaseCouldBeUsedForBuyout) && (
<Flex gap={2}>
{avatarInfo.rentCouldBeUsedForBuyout && (
<Text pointer small underline variant="white" onClick={() => CreateLinkEvent(`rent-furni/extend/${avatarInfo.id}/${avatarInfo.category}`)}>
{localizeWithFallback('infostand.button.extend', 'Extend')}
</Text>
)}
{avatarInfo.purchaseCouldBeUsedForBuyout && (
<Text pointer small underline variant="white" onClick={() => CreateLinkEvent(`rent-furni/buyout/${avatarInfo.id}/${avatarInfo.category}`)}>
{localizeWithFallback('infostand.button.buyout', 'Buy-out')}
</Text>
)}
</Flex>
)}
{avatarInfo.expiration < 0 && avatarInfo.rentOfferId > 0 && (
<Flex>
<Text pointer small underline variant="white" onClick={() => CreateLinkEvent(`catalog/open/offerId/${avatarInfo.rentOfferId}`)}>
{localizeWithFallback('infostand.button.rent', 'Rent one')}
</Text>
</Flex>
)}
</div>
{(isJukeBox || isSongDisk) && (
<div className="flex flex-col gap-1">
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
{songId === -1 && (
<Text small wrap variant="white">
{LocalizeText('infostand.jukebox.text.not.playing')}
</Text>
)}
{!!songName.length && (
<div className="flex items-center gap-1">
<div className="icon disk-icon" />
<Text small wrap variant="white">
{songName}
</Text>
</div>
)}
{!!songCreator.length && (
<div className="flex items-center gap-1">
<div className="icon disk-creator" />
<Text small wrap variant="white">
{songCreator}
</Text>
</div>
)}
</div>
)}
<div className="flex flex-col gap-1">
{isCrackable && (
<>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
<Text small wrap variant="white">
{LocalizeText(
'infostand.crackable_furni.hits_remaining',
['hits', 'target'],
[(crackableHits ?? 0).toString(), (crackableTarget ?? 0).toString()]
)}
</Text>
</>
)}
{isPlant && (
<>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
{plantDead ? (
<Text small wrap variant="danger">
{LocalizeText('infostand.plant.dead')}
</Text>
) : (
<div className="flex flex-col gap-1">
<Flex alignItems="center" gap={1} justifyContent="between">
<Text small wrap variant="white">
{LocalizeText('infostand.plant.rewater')}
</Text>
<Text small wrap variant={plantWaterSeconds > 0 ? 'white' : 'success'}>
{plantWaterSeconds > 0
? formatPlantDuration(plantWaterSeconds)
: LocalizeText('infostand.plant.ready')}
</Text>
</Flex>
<Flex alignItems="center" gap={1} justifyContent="between">
<Text small wrap variant="white">
{LocalizeText('infostand.plant.dies')}
</Text>
<Text small wrap variant={plantDeathSeconds > 0 ? 'white' : 'danger'}>
{plantDeathSeconds > 0
? formatPlantDuration(plantDeathSeconds)
: LocalizeText('infostand.plant.needswater')}
</Text>
</Flex>
</div>
)}
</>
)}
{avatarInfo.groupId > 0 && (
<>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
<Flex pointer alignItems="center" gap={2} onClick={() => GetGroupInformation(avatarInfo.groupId)}>
{!!groupBadge && <LayoutBadgeImageView badgeCode={groupBadge} isGroup={true} />}
<Text underline variant="white">
{groupName}
</Text>
</Flex>
</>
)}
{(showLocation || showIds) && (
<>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
<div className="grid grid-cols-2 gap-x-3 gap-y-0">
{showLocation && (
<>
<InfoStandStat icon={<StatIcon src={statXIcon} />} label="X" value={itemLocation.x} />
<InfoStandStat iconAfter icon={<StatIcon src={statRotationIcon} />} label="Rotation (0-7)" value={furniDirection} />
<InfoStandStat icon={<StatIcon src={statYIcon} />} label="Y" value={itemLocation.y} />
<InfoStandStat iconAfter icon={<StatIcon src={statStateIcon} />} label="State" value={furniState} />
<InfoStandStat icon={<StatIcon src={statHeightIcon} />} label="H" value={itemLocation.z < 0.01 ? 0 : Number(itemLocation.z.toFixed(2))} />
</>
)}
{showIds && (
<div className={showLocation ? '' : 'col-span-2'}>
<InfoStandStat
iconAfter
icon={<StatIcon src={statIdIcon} />}
label="ID"
tooltip={`ID (Sprite ${furniTypeId})`}
value={avatarInfo.id}
/>
</div>
)}
</div>
</>
)}
{rareValue && rareValue.points > 0 && (
<>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
<Flex alignItems="center" gap={2}>
<Text small variant="white">
{LocalizeText('rarevalues.infostand.label')}
</Text>
<Flex alignItems="center" gap={1}>
<Text small variant="white">
{rareValue.points}
</Text>
<LayoutCurrencyIcon type={rareValue.pointsType} />
</Flex>
</Flex>
</>
)}
{(showEditFurni || showBuildtools) && (
<>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
{(showEditFurni || showBuildtools) && (
<div className="flex gap-1 w-full">
{showEditFurni && (
<BaseButton
className="flex-1 min-w-0 text-white text-xs bg-[#418db0] hover:bg-[#3789a8] border border-[#ffffff33] rounded px-2 py-1 cursor-pointer transition-colors"
onClick={() => {
CreateLinkEvent('furni-editor/show');
if (furniTypeId !== '?') window.dispatchEvent(new CustomEvent('furni-editor:open', { detail: { spriteId: furniTypeId } }));
}}
>
{localizeWithFallback('infostand.button.edit_furni', 'Edit furni')}
</BaseButton>
)}
{showBuildtools && (
<BaseButton
className="flex-1 min-w-0 text-white text-xs bg-[#2a2a3a] hover:bg-[#3a3a4a] border border-[#ffffff33] rounded px-2 py-1 cursor-pointer transition-colors"
onClick={() => setDropdownOpen(!dropdownOpen)}
>
{dropdownOpen
? localizeWithFallback('infostand.button.buildtools.close', 'Close Buildtools')
: localizeWithFallback('infostand.button.buildtools.open', 'Open Buildtools')}
</BaseButton>
)}
</div>
)}
{showBuildtools && (
<>
{dropdownOpen && (
<div className="flex gap-[4px] w-full">
<div className="flex-1 bg-[#3D5D63] rounded-[6px] border border-white p-[2px] flex flex-col gap-1">
<Text small variant="white">
{LocalizeText('group.edit.badge.position')}
</Text>
<div className="flex flex-col items-center gap-[2px]">
<div className="flex gap-[0.6em]">
<div
className="bg-[#E55959] text-white w-[25px] h-[25px] border border-white cursor-pointer rounded-[3px] flex justify-center items-center transition-[filter] duration-300 hover:brightness-150 rotate-[225deg]"
onClick={() => sendUpdate(-1, 0, furniLocationZ ?? 0, 0)}
>
<GrFormNextLink size="1.7em" />
</div>
<div
className="bg-[#E55959] text-white w-[25px] h-[25px] border border-white cursor-pointer rounded-[3px] flex justify-center items-center transition-[filter] duration-300 hover:brightness-150 rotate-[315deg]"
onClick={() => sendUpdate(0, -1, furniLocationZ ?? 0, 0)}
>
<GrFormNextLink size="1.7em" />
</div>
</div>
<div className="flex gap-[0.6em]">
<div
className="bg-[#E55959] text-white w-[25px] h-[25px] border border-white cursor-pointer rounded-[3px] flex justify-center items-center transition-[filter] duration-300 hover:brightness-150 rotate-[135deg]"
onClick={() => sendUpdate(0, 1, furniLocationZ ?? 0, 0)}
>
<GrFormNextLink size="1.7em" />
</div>
<div
className="bg-[#E55959] text-white w-[25px] h-[25px] border border-white cursor-pointer rounded-[3px] flex justify-center items-center transition-[filter] duration-300 hover:brightness-150 rotate-[45deg]"
onClick={() => sendUpdate(1, 0, furniLocationZ ?? 0, 0)}
>
<GrFormNextLink size="1.7em" />
</div>
</div>
</div>
<Text small variant="white">
{LocalizeText('infostand.button.rotate')}
</Text>
<div className="flex justify-center gap-[0.6em]">
<div
className="bg-[#D1A245] text-black w-[28px] h-[28px] border-2 border-[#eee] cursor-pointer rounded-full flex justify-center items-center transition-[filter] duration-300 hover:brightness-150"
onClick={() => sendUpdate(0, 0, furniLocationZ ?? 0, -1)}
>
<GrRotateLeft size="1.4em" />
</div>
<div
className="bg-[#D1A245] text-black w-[28px] h-[28px] border-2 border-[#eee] cursor-pointer rounded-full flex justify-center items-center transition-[filter] duration-300 hover:brightness-150"
onClick={() => sendUpdate(0, 0, furniLocationZ ?? 0, 1)}
>
<GrRotateRight size="1.4em" />
</div>
</div>
</div>
<div className="flex-1 bg-[#3D5D63] rounded-[6px] border border-white p-[2px] flex flex-col gap-1">
<Text small variant="white">
{LocalizeText('stack.magic.tile.height.label')}
</Text>
<NumberInput spellCheck={false} className="w-full text-xs bg-[#1a1a2a] text-white border border-[#ffffff44] rounded px-1 py-0.5" value={furniLocationZ !== null ? furniLocationZ.toString() : ''} onChange={handleHeightChange} onBlur={handleHeightBlur} min={0} max={40} step={0.1} />
<div className="flex justify-center gap-1">
<div className="flex flex-col items-center gap-[2px]">
<div
className="bg-[#247FD1] text-white w-[24px] h-[24px] border border-white cursor-pointer rounded-[3px] leading-none flex justify-center items-center transition-[filter] duration-300 hover:brightness-150"
onClick={() => adjustHeight(1)}
>
↑
</div>
<Text small variant="white" align="center">
█
</Text>
<div
className="bg-[#44A750] text-white w-[24px] h-[24px] border border-white cursor-pointer rounded-[3px] leading-none flex justify-center items-center transition-[filter] duration-300 hover:brightness-150"
onClick={() => adjustHeight(-1)}
>
↓
</div>
</div>
<div className="flex flex-col items-center gap-[2px]">
<div
className="bg-[#247FD1] text-white w-[24px] h-[24px] border border-white cursor-pointer rounded-[3px] leading-none flex justify-center items-center transition-[filter] duration-300 hover:brightness-150"
onClick={() => adjustHeight(0.1)}
>
↑
</div>
<Text small variant="white" align="center">
▄
</Text>
<div
className="bg-[#44A750] text-white w-[24px] h-[24px] border border-white cursor-pointer rounded-[3px] leading-none flex justify-center items-center transition-[filter] duration-300 hover:brightness-150"
onClick={() => adjustHeight(-0.1)}
>
↓
</div>
</div>
<div className="flex flex-col items-center gap-[2px]">
<div
className="bg-[#247FD1] text-white w-[24px] h-[24px] border border-white cursor-pointer rounded-[3px] leading-none flex justify-center items-center transition-[filter] duration-300 hover:brightness-150"
onClick={() => adjustHeight(0.01)}
>
↑
</div>
<Text small variant="white" align="center">
_
</Text>
<div
className="bg-[#44A750] text-white w-[24px] h-[24px] border border-white cursor-pointer rounded-[3px] leading-none flex justify-center items-center transition-[filter] duration-300 hover:brightness-150"
onClick={() => adjustHeight(-0.01)}
>
↓
</div>
</div>
</div>
</div>
</div>
)}
</>
)}
{furniKeys.length > 0 && (
<>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
<div className="flex flex-col gap-1">
{furniKeys.map((key, index) => {
const range = BRANDING_NUMBER_RANGES[key];
return (
<Flex key={index} column gap={0}>
<Text small wrap variant="white">
{key}
</Text>
{range ? (
<NumberInput
className="form-control form-control-sm text-black"
style={{ color: '#000' }}
min={range.min}
max={range.max}
value={furniValues[index]}
onChange={(event) => onFurniSettingChange(index, event.target.value)}
/>
) : (
<OctaneInput
type="text"
className="text-black"
style={{ color: '#000' }}
placeholder={key === 'imageUrl' || key === 'clickUrl' ? 'https://' : undefined}
value={furniValues[index]}
onChange={(event) => onFurniSettingChange(index, event.target.value)}
/>
)}
</Flex>
);
})}
</div>
</>
)}
</>
)}
{customKeys.length > 0 && (
<>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
<div className="flex flex-col gap-1">
{customKeys.map((key, index) => {
return (
<Flex key={index} alignItems="center" gap={1}>
<Text small wrap align="end" className="col-span-4" variant="white">
{key}
</Text>
<OctaneInput
type="text"
className="text-black"
style={{ color: '#000' }}
value={customValues[index]}
onChange={(event) => onCustomVariableChange(index, event.target.value)}
/>
</Flex>
);
})}
</div>
</>
)}
</div>
</Column>
</Column>
<Flex gap={1} justifyContent="end">
{showInspectButton && !roomSession?.playTestMode && (
<Button variant="dark" onClick={() => openInspectionForFurni(avatarInfo.id, avatarInfo.category)}>
Inspect
</Button>
)}
{canMove && (
<Button variant="dark" onClick={(event) => processButtonAction('move')}>
{LocalizeText('infostand.button.move')}
</Button>
)}
{canRotate && (
<Button variant="dark" onClick={(event) => processButtonAction('rotate')}>
{LocalizeText('infostand.button.rotate')}
</Button>
)}
{hasLabel && !!roomSettings?.canModify && (
<Button variant="dark" onClick={() => openLabelSettings(avatarInfo.id)}>
{localizeWithFallback('infostand.button.label_settings', 'Settings')}
</Button>
)}
{canRecolor && (
<Button variant="dark" onClick={() => openRecolor && openRecolor(avatarInfo.id)}>
{localizeWithFallback('infostand.button.recolor', 'Recolor')}
</Button>
)}
{pickupMode !== PICKUP_MODE_NONE && (
<Button variant="dark" onClick={(event) => processButtonAction('pickup')}>
{isPlant && plantDead
? LocalizeText('generic.delete')
: LocalizeText(pickupMode === PICKUP_MODE_EJECT ? 'infostand.button.eject' : 'infostand.button.pickup')}
</Button>
)}
{canUse && (
<Button variant="dark" onClick={(event) => processButtonAction('use')}>
{LocalizeText('infostand.button.use')}
</Button>
)}
{canRemoveBackground && (
<Button variant="dark" onClick={(event) => processButtonAction('remove_background')}>
<Flex alignItems="center" gap={1}>
<FaEraser className="fa-icon" />
{removeLandscapeLabel()}
</Flex>
</Button>
)}
{hasBrandingOffsets && (
<Button variant="dark" onClick={() => setShowPositionEditor(true)}>
{LocalizeText('image.position.editor.button')}
</Button>
)}
{furniKeys.length > 0 && furniValues.length > 0 && furniKeys.length === furniValues.length && (
<Button variant="dark" onClick={() => processButtonAction('save_branding_configuration')}>
{LocalizeText('save')}
</Button>
)}
{customKeys.length > 0 && customValues.length > 0 && customKeys.length === customValues.length && (
<Button variant="dark" onClick={() => processButtonAction('save_custom_variables')}>
{LocalizeText('save')}
</Button>
)}
</Flex>
{showPositionEditor && (
<ImagePositionEditorView
roomId={roomSession.roomId}
objectId={avatarInfo.id}
isWallItem={avatarInfo.isWallItem}
initialX={getBrandingOffset('offsetX')}
initialY={getBrandingOffset('offsetY')}
initialZ={getBrandingOffset('offsetZ')}
initialScale={getBrandingOffset('scale') || 100}
initialAlpha={furniKeys.indexOf('alpha') >= 0 ? getBrandingOffset('alpha') : 100}
onClose={() => setShowPositionEditor(false)}
onSave={savePositionEditor}
/>
)}
</Column>
</NativeTextTree>
);
};
