Merge remote-tracking branch 'duckie/main' into merge-duckie-main-2026-05-06

# Conflicts:
#	index.html
#	public/UITexts.example
#	public/renderer-config.example
#	src/App.tsx
#	src/components/login/LoginView.tsx
#	src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx
#	src/components/toolbar/ToolbarView.tsx
#	src/components/user-profile/UserContainerView.tsx
This commit is contained in:
Lorenzune
2026-05-06 04:23:15 +02:00
57 changed files with 5714 additions and 3013 deletions
@@ -1,4 +1,5 @@
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { FaPlus } from 'react-icons/fa';
import { GetConfigurationValue, LocalizeText } from '../../../../../api';
import { LayoutBadgeImageView } from '../../../../../common';
@@ -15,7 +16,8 @@ const BadgeMiniPicker: FC<{
onSelect: (badgeCode: string) => void;
onClose: () => void;
activeBadgeCodes: (string | null)[];
}> = ({ onSelect, onClose, activeBadgeCodes }) =>
position: { top: number; left: number };
}> = ({ onSelect, onClose, activeBadgeCodes, position }) =>
{
const { badgeCodes = [], requestBadges = null } = useInventoryBadges();
const ref = useRef<HTMLDivElement>(null);
@@ -43,10 +45,11 @@ const BadgeMiniPicker: FC<{
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [ onClose ]);
return (
return createPortal(
<div
ref={ ref }
className="absolute right-[calc(100%+8px)] top-0 z-50 bg-[rgba(28,28,32,0.97)] border border-white/20 rounded-md p-2 shadow-lg min-w-[160px]"
className="fixed z-[9999] bg-[rgba(28,28,32,0.97)] border border-white/20 rounded-md p-2 shadow-lg min-w-[160px]"
style={ { top: position.top, left: position.left } }
onClick={ e => e.stopPropagation() }>
<input
autoFocus
@@ -73,7 +76,8 @@ const BadgeMiniPicker: FC<{
) }
</div>
) }
</div>
</div>,
document.body
);
};
@@ -83,7 +87,9 @@ export const InfoStandBadgeSlotView: FC<InfoStandBadgeSlotProps> = ({ slotIndex,
const [ isDragOver, setIsDragOver ] = useState(false);
const [ isDragging, setIsDragging ] = useState(false);
const [ justDropped, setJustDropped ] = useState(false);
const [ showPicker, setShowPicker ] = useState(false);
const [ pickerPosition, setPickerPosition ] = useState<{ top: number; left: number } | null>(null);
const slotRef = useRef<HTMLDivElement>(null);
const showPicker = pickerPosition !== null;
const hookInitialized = activeBadgeCodes.length > 0;
@@ -152,9 +158,17 @@ export const InfoStandBadgeSlotView: FC<InfoStandBadgeSlotProps> = ({ slotIndex,
const handleSlotClick = useCallback(() =>
{
if(!isOwnUser || badgeCode) return;
if(!isOwnUser || badgeCode || !slotRef.current) return;
setShowPicker(true);
const rect = slotRef.current.getBoundingClientRect();
const pickerWidth = 180;
const gap = 8;
let left = rect.right + gap;
if((left + pickerWidth) > (window.innerWidth - gap)) left = rect.left - pickerWidth - gap;
if(left < gap) left = gap;
setPickerPosition({ top: rect.top, left });
}, [ isOwnUser, badgeCode ]);
const handleDoubleClick = useCallback(() =>
@@ -167,12 +181,13 @@ export const InfoStandBadgeSlotView: FC<InfoStandBadgeSlotProps> = ({ slotIndex,
const handlePickerSelect = useCallback((code: string) =>
{
setBadgeAtSlot(code, slotIndex);
setShowPicker(false);
setPickerPosition(null);
}, [ setBadgeAtSlot, slotIndex ]);
return (
<div className="relative">
<div
ref={ slotRef }
className={ `flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center transition-all duration-150
${ isOwnUser && badgeCode ? 'cursor-grab active:cursor-grabbing' : '' }
${ isOwnUser && !badgeCode ? 'cursor-pointer' : '' }
@@ -196,8 +211,9 @@ export const InfoStandBadgeSlotView: FC<InfoStandBadgeSlotProps> = ({ slotIndex,
{ showPicker && (
<BadgeMiniPicker
activeBadgeCodes={ activeBadgeCodes }
onClose={ () => setShowPicker(false) }
onClose={ () => setPickerPosition(null) }
onSelect={ handlePickerSelect }
position={ pickerPosition }
/>
) }
</div>
@@ -1,4 +1,4 @@
import { CrackableDataType, CreateLinkEvent, FurnitureFloorUpdateEvent, GetRoomEngine, GetSoundManager, GroupInformationComposer, GroupInformationEvent, NowPlayingEvent, RoomControllerLevel, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomWidgetEnumItemExtradataParameter, RoomWidgetFurniInfoUsagePolicyEnum, SetObjectDataMessageComposer, SongInfoReceivedEvent, StringDataType, UpdateFurniturePositionComposer } from '@nitrots/nitro-renderer';
import { CrackableDataType, CreateLinkEvent, FurnitureFloorUpdateEvent, GetRoomEngine, GetSessionDataManager, GetSoundManager, GroupInformationComposer, GroupInformationEvent, NowPlayingEvent, RoomControllerLevel, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomWidgetEnumItemExtradataParameter, RoomWidgetFurniInfoUsagePolicyEnum, SetObjectDataMessageComposer, SongInfoReceivedEvent, StringDataType, UpdateFurniturePositionComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { FaCrosshairs, FaRulerVertical, FaTimes } from 'react-icons/fa';
import { GrFormNextLink, GrRotateLeft, GrRotateRight } from 'react-icons/gr';
@@ -585,19 +585,20 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
onClick={ () => setDropdownOpen(!dropdownOpen) }>
{ dropdownOpen ? `${LocalizeText('widget.furni.present.close')} Buildtools` : `${LocalizeText('navigator.roomsettings.doormode.open')} Buildtools` }
</button>
<button
className="w-full text-white text-xs bg-[#1e7295] hover:bg-[#1a617f] border border-[#ffffff33] rounded px-2 py-1 cursor-pointer transition-colors"
onClick={ () =>
{
const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR);
const typeId = roomObject?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID);
{ GetSessionDataManager().isModerator &&
<button
className="w-full text-white text-xs bg-[#1e7295] hover:bg-[#1a617f] border border-[#ffffff33] rounded px-2 py-1 cursor-pointer transition-colors"
onClick={ () =>
{
const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR);
const typeId = roomObject?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID);
CreateLinkEvent('furni-editor/show');
CreateLinkEvent('furni-editor/show');
if(typeId) window.dispatchEvent(new CustomEvent('furni-editor:open', { detail: { spriteId: typeId } }));
} }>
Edit Furni
</button>
if(typeId) window.dispatchEvent(new CustomEvent('furni-editor:open', { detail: { spriteId: typeId } }));
} }>
Edit Furni
</button> }
{ dropdownOpen &&
<div className="flex gap-[4px] w-full">
{ /* Left panel: position + rotation */ }
@@ -24,12 +24,14 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
const [backgroundId, setBackgroundId] = useState<number>(null);
const [standId, setStandId] = useState<number>(null);
const [overlayId, setOverlayId] = useState<number>(null);
const [cardBackgroundId, setCardBackgroundId] = useState<number>(null);
const [isVisible, setIsVisible] = useState(false);
const { roomSession = null } = useRoom();
const infostandBackgroundClass = `background-${backgroundId ?? 'default'}`;
const infostandStandClass = `stand-${standId ?? 'default'}`;
const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`;
const infostandCardBackgroundClass = cardBackgroundId ? `card-background-${cardBackgroundId}` : '';
const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]);
const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); setIsVisible(prev => !prev); }, []);
@@ -96,6 +98,7 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
newValue.backgroundId = event.backgroundId;
newValue.standId = event.standId;
newValue.overlayId = event.overlayId;
newValue.cardBackgroundId = event.cardBackgroundId ?? 0;
return newValue;
});
});
@@ -130,16 +133,12 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
setBackgroundId(avatarInfo.backgroundId);
setStandId(avatarInfo.standId);
setOverlayId(avatarInfo.overlayId);
setCardBackgroundId(avatarInfo.cardBackgroundId ?? 0);
SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID));
return () => {
setIsEditingMotto(false);
setMotto(null);
setRelationships(null);
setBackgroundId(null);
setStandId(null);
setOverlayId(null);
};
}, [avatarInfo]);
@@ -147,7 +146,7 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
return (
<>
<Column className="relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto bg-[rgba(28,28,32,0.95)] [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded">
<Column className={`relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto ${cardBackgroundId ? '' : 'bg-[rgba(28,28,32,0.95)]'} [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded overflow-hidden profile-card-background ${infostandCardBackgroundClass}`}>
<Column className="h-full p-[8px] overflow-auto" gap={1} overflow="visible">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
@@ -292,6 +291,8 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
setSelectedStand={setStandId}
selectedOverlay={overlayId}
setSelectedOverlay={setOverlayId}
selectedCardBackground={cardBackgroundId}
setSelectedCardBackground={setCardBackgroundId}
/>
</div>
)}
@@ -55,6 +55,9 @@ export const AvatarInfoWidgetOwnAvatarView: FC<AvatarInfoWidgetOwnAvatarViewProp
case 'change_looks':
CreateLinkEvent('avatar-editor/show');
break;
case 'avatar_effect':
CreateLinkEvent('avatar-effects/show');
break;
case 'expressions':
hideMenu = false;
setMode(MODE_EXPRESSIONS);
@@ -137,6 +140,9 @@ export const AvatarInfoWidgetOwnAvatarView: FC<AvatarInfoWidgetOwnAvatarViewProp
<ContextMenuListItemView onClick={ event => processAction('change_looks') }>
{ LocalizeText('widget.memenu.myclothes') }
</ContextMenuListItemView>
<ContextMenuListItemView onClick={ event => processAction('avatar_effect') }>
{ LocalizeText('product.type.effect') }
</ContextMenuListItemView>
{ (HasHabboClub() && !isRidingHorse) &&
<ContextMenuListItemView onClick={ event => processAction('dance_menu') }>
<FaChevronRight className="right fa-icon" />