🆙 Init V3

This commit is contained in:
DuckieTM
2026-01-31 09:10:52 +01:00
commit 7feb10ab15
1733 changed files with 53405 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
import { AvatarScaleType, AvatarSetType, GetAvatarRenderManager } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from 'react';
import { Base, BaseProps } from '../Base';
const AVATAR_IMAGE_CACHE: Map<string, string> = new Map();
export interface LayoutAvatarImageViewProps extends BaseProps<HTMLDivElement>
{
figure: string;
gender?: string;
headOnly?: boolean;
direction?: number;
scale?: number;
}
export const LayoutAvatarImageView: FC<LayoutAvatarImageViewProps> = props =>
{
const { figure = '', gender = 'M', headOnly = false, direction = 0, scale = 1, classNames = [], style = {}, ...rest } = props;
const [ avatarUrl, setAvatarUrl ] = useState<string>(null);
const [ isReady, setIsReady ] = useState<boolean>(false);
const isDisposed = useRef(false);
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'avatar-image relative w-[90px] h-[130px] bg-no-repeat bg-[center_-8px] pointer-events-none' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
if(avatarUrl && avatarUrl.length) newStyle.backgroundImage = `url('${ avatarUrl }')`;
if(scale !== 1)
{
newStyle.transform = `scale(${ scale })`;
if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
}
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ avatarUrl, scale, style ]);
useEffect(() =>
{
if(!isReady) return;
const figureKey = [ figure, gender, direction, headOnly ].join('-');
if(AVATAR_IMAGE_CACHE.has(figureKey))
{
setAvatarUrl(AVATAR_IMAGE_CACHE.get(figureKey));
}
else
{
const resetFigure = (_figure: string) =>
{
if(isDisposed.current) return;
const avatarImage = GetAvatarRenderManager().createAvatarImage(_figure, AvatarScaleType.LARGE, gender, { resetFigure: (figure: string) => resetFigure(figure), dispose: null, disposed: false });
let setType = AvatarSetType.FULL;
if(headOnly) setType = AvatarSetType.HEAD;
avatarImage.setDirection(setType, direction);
const imageUrl = avatarImage.processAsImageUrl(setType);
if(imageUrl && !isDisposed.current)
{
if(!avatarImage.isPlaceholder()) AVATAR_IMAGE_CACHE.set(figureKey, imageUrl);
setAvatarUrl(imageUrl);
}
avatarImage.dispose();
};
resetFigure(figure);
}
}, [ figure, gender, direction, headOnly, isReady ]);
useEffect(() =>
{
isDisposed.current = false;
setIsReady(true);
return () =>
{
isDisposed.current = true;
};
}, []);
return <Base classNames={ getClassNames } style={ getStyle } { ...rest } />;
};
@@ -0,0 +1,23 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from '../Base';
export interface LayoutBackgroundImageProps extends BaseProps<HTMLDivElement>
{
imageUrl?: string;
}
export const LayoutBackgroundImage: FC<LayoutBackgroundImageProps> = props =>
{
const { imageUrl = null, fit = true, style = null, ...rest } = props;
const getStyle = useMemo(() =>
{
const newStyle = { ...style };
if(imageUrl) newStyle.background = `url(${ imageUrl }) center no-repeat`;
return newStyle;
}, [ style, imageUrl ]);
return <Base fit={ fit } style={ getStyle } { ...rest } />;
};
+109
View File
@@ -0,0 +1,109 @@
import { BadgeImageReadyEvent, GetEventDispatcher, GetSessionDataManager, NitroSprite, TextureUtils } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, useEffect, useMemo, useState } from 'react';
import { GetConfigurationValue, LocalizeBadgeDescription, LocalizeBadgeName, LocalizeText } from '../../api';
import { Base, BaseProps } from '../Base';
export interface LayoutBadgeImageViewProps extends BaseProps<HTMLDivElement>
{
badgeCode: string;
isGroup?: boolean;
showInfo?: boolean;
customTitle?: string;
isGrayscale?: boolean;
scale?: number;
}
export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
{
const { badgeCode = null, isGroup = false, showInfo = false, customTitle = null, isGrayscale = false, scale = 1, classNames = [], style = {}, children = null, ...rest } = props;
const [ imageElement, setImageElement ] = useState<HTMLImageElement>(null);
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'relative w-[40px] h-[40px] bg-no-repeat bg-center' ];
if(isGroup) newClassNames.push('group-badge');
if(isGrayscale) newClassNames.push('grayscale');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames, isGroup, isGrayscale ]);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
if(imageElement)
{
newStyle.backgroundImage = `url(${ (isGroup) ? imageElement.src : GetConfigurationValue<string>('badge.asset.url').replace('%badgename%', badgeCode.toString()) })`;
newStyle.width = imageElement.width;
newStyle.height = imageElement.height;
if(scale !== 1)
{
newStyle.transform = `scale(${ scale })`;
if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
newStyle.width = (imageElement.width * scale);
newStyle.height = (imageElement.height * scale);
}
}
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ badgeCode, isGroup, imageElement, scale, style ]);
useEffect(() =>
{
if(!badgeCode || !badgeCode.length) return;
let didSetBadge = false;
const onBadgeImageReadyEvent = async (event: BadgeImageReadyEvent) =>
{
if(event.badgeId !== badgeCode) return;
const element = await TextureUtils.generateImage(new NitroSprite(event.image));
console.log ('boe');
element.onload = () => setImageElement(element);
didSetBadge = true;
GetEventDispatcher().removeEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
};
GetEventDispatcher().addEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
const texture = isGroup ? GetSessionDataManager().getGroupBadgeImage(badgeCode) : GetSessionDataManager().getBadgeImage(badgeCode);
if(texture && !didSetBadge)
{
(async () =>
{
const element = await TextureUtils.generateImage(new NitroSprite(texture));
element.onload = () => setImageElement(element);
})();
}
return () => GetEventDispatcher().removeEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
}, [ badgeCode, isGroup ]);
return (
<Base className="group" classNames={ getClassNames } style={ getStyle } { ...rest }>
{ (showInfo && GetConfigurationValue<boolean>('badge.descriptions.enabled', true)) &&
<Base className="hidden group-hover:block before:absolute before:content-['_'] before:w-[0] before:h-[0] before:!border-l-[10px] before:!border-b-[10px] before:!border-t-[10px] before:top-[10px] before:-right-[10px] before:[border-left-color:white] before:[border-top-color:transparent] before:[border-bottom-color:transparent] z-50 absolute pointer-events-none select-none w-[210px] rounded-[.25rem] bg-[#fff] -left-[220px] text-black py-1 px-2 small">
<div className="font-bold mb-1">{ isGroup ? customTitle : LocalizeBadgeName(badgeCode) }</div>
<div>{ isGroup ? LocalizeText('group.badgepopup.body') : LocalizeBadgeDescription(badgeCode) }</div>
</Base> }
{ children }
</Base>
);
};
@@ -0,0 +1,42 @@
import { FC, useMemo } from 'react';
import { LocalizeText } from '../../api';
import { Base, BaseProps } from '../Base';
interface LayoutCounterTimeViewProps extends BaseProps<HTMLDivElement>
{
day: string;
hour: string;
minutes: string;
seconds: string;
}
export const LayoutCounterTimeView: FC<LayoutCounterTimeViewProps> = props =>
{
const { day = '00', hour = '00', minutes = '00', seconds = '00', classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'nitro-counter-time' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<div className="flex gap-1 top-2 end-2">
<Base classNames={ getClassNames } { ...rest }>
<div>{ day != '00' ? day : hour }{ day != '00' ? LocalizeText('countdown_clock_unit_days') : LocalizeText('countdown_clock_unit_hours') }</div>
</Base>
<div style={ { marginTop: '3px' } }>:</div>
<Base className="nitro-counter-time" { ...rest }>
<div>{ minutes }{ LocalizeText('countdown_clock_unit_minutes') }</div>
</Base>
<Base style={ { marginTop: '3px' } }>:</Base>
<Base className="nitro-counter-time" { ...rest }>
<div>{ seconds }{ LocalizeText('countdown_clock_unit_seconds') }</div>
</Base>
{ children }
</div>
);
};
+44
View File
@@ -0,0 +1,44 @@
import { CSSProperties, FC, useMemo } from 'react';
import { GetConfigurationValue } from '../../api';
import { Base, BaseProps } from '../Base';
export interface CurrencyIconProps extends BaseProps<HTMLDivElement>
{
type: number | string;
}
export const LayoutCurrencyIcon: FC<CurrencyIconProps> = props =>
{
const { type = '', classNames = [], style = {}, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'nitro-currency-icon', 'bg-center bg-no-repeat w-[15px] h-[15px]' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
const urlString = useMemo(() =>
{
let url = GetConfigurationValue<string>('currency.asset.icon.url', '');
url = url.replace('%type%', type.toString());
return `url(${ url })`;
}, [ type ]);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
newStyle.backgroundImage = urlString;
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ style, urlString ]);
return <Base classNames={ getClassNames } style={ getStyle } { ...rest } />;
};
@@ -0,0 +1,17 @@
import { FC } from 'react';
import { GetImageIconUrlForProduct } from '../../api';
import { LayoutImage, LayoutImageProps } from './LayoutImage';
interface LayoutFurniIconImageViewProps extends LayoutImageProps
{
productType: string;
productClassId: number;
extraData?: string;
}
export const LayoutFurniIconImageView: FC<LayoutFurniIconImageViewProps> = props =>
{
const { productType = 's', productClassId = -1, extraData = '', ...rest } = props;
return <LayoutImage className="furni-image" imageUrl={ GetImageIconUrlForProduct(productType, productClassId, extraData) } { ...rest } />;
};
@@ -0,0 +1,70 @@
import { GetRoomEngine, IGetImageListener, ImageResult, TextureUtils, Vector3d } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, useEffect, useMemo, useState } from 'react';
import { ProductTypeEnum } from '../../api';
import { Base, BaseProps } from '../Base';
interface LayoutFurniImageViewProps extends BaseProps<HTMLDivElement>
{
productType: string;
productClassId: number;
direction?: number;
extraData?: string;
scale?: number;
}
export const LayoutFurniImageView: FC<LayoutFurniImageViewProps> = props =>
{
const { productType = 's', productClassId = -1, direction = 2, extraData = '', scale = 1, style = {}, ...rest } = props;
const [ imageElement, setImageElement ] = useState<HTMLImageElement>(null);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
if(imageElement?.src?.length)
{
newStyle.backgroundImage = `url('${ imageElement.src }')`;
newStyle.width = imageElement.width;
newStyle.height = imageElement.height;
}
if(scale !== 1)
{
newStyle.transform = `scale(${ scale })`;
if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
}
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ imageElement, scale, style ]);
useEffect(() =>
{
let imageResult: ImageResult = null;
const listener: IGetImageListener = {
imageReady: async (id, texture, image) => setImageElement(await TextureUtils.generateImage(texture)),
imageFailed: null
};
switch(productType.toLocaleLowerCase())
{
case ProductTypeEnum.FLOOR:
imageResult = GetRoomEngine().getFurnitureFloorImage(productClassId, new Vector3d(direction), 64, listener, 0, extraData);
break;
case ProductTypeEnum.WALL:
imageResult = GetRoomEngine().getFurnitureWallImage(productClassId, new Vector3d(direction), 64, listener, 0, extraData);
break;
}
if(!imageResult) return;
(async () => setImageElement(await TextureUtils.generateImage(imageResult.data)))();
}, [ productType, productClassId, direction, extraData ]);
if(!imageElement) return null;
return <Base classNames={ [ 'furni-image' ] } style={ getStyle } { ...rest } />;
};
+41
View File
@@ -0,0 +1,41 @@
import { FC } from 'react';
import { LocalizeText } from '../../api';
import { Column } from '../Column';
import { Flex } from '../Flex';
import { Text } from '../Text';
import { LayoutAvatarImageView } from './LayoutAvatarImageView';
interface LayoutGiftTagViewProps
{
figure?: string;
userName?: string;
message?: string;
editable?: boolean;
onChange?: (value: string) => void;
}
export const LayoutGiftTagView: FC<LayoutGiftTagViewProps> = props =>
{
const { figure = null, userName = null, message = null, editable = false, onChange = null } = props;
return (
<Flex className="nitro-gift-card text-black" overflow="hidden">
<div className="flex items-center justify-center gift-face flex-shrink-0">
{ !userName && <div className="gift-incognito"></div> }
{ figure && <div className="gift-avatar">
<LayoutAvatarImageView direction={ 2 } figure={ figure } headOnly={ true } />
</div> }
</div>
<Flex className="w-full pt-4 pb-4 pe-4 ps-3" overflow="hidden">
<Column className="!flex-grow" justifyContent="between" overflow="auto">
{ !editable &&
<Text textBreak className="gift-message">{ message }</Text> }
{ editable && (onChange !== null) &&
<textarea className="gift-message h-full" maxLength={ 140 } placeholder={ LocalizeText('catalog.gift_wrapping_new.message_hint') } value={ message } onChange={ (e) => onChange(e.target.value) }></textarea> }
{ userName &&
<Text italics textEnd className="pe-1">{ LocalizeText('catalog.gift_wrapping_new.message_from', [ 'name' ], [ userName ]) }</Text> }
</Column>
</Flex>
</Flex>
);
};
+76
View File
@@ -0,0 +1,76 @@
import { FC, useMemo } from 'react';
import { Base } from '../Base';
import { Column, ColumnProps } from '../Column';
import { LayoutItemCountView } from './LayoutItemCountView';
import { LayoutLimitedEditionStyledNumberView } from './limited-edition';
export interface LayoutGridItemProps extends ColumnProps
{
itemImage?: string;
itemColor?: string;
itemActive?: boolean;
itemCount?: number;
itemCountMinimum?: number;
itemUniqueSoldout?: boolean;
itemUniqueNumber?: number;
itemUnseen?: boolean;
itemHighlight?: boolean;
disabled?: boolean;
}
export const LayoutGridItem: FC<LayoutGridItemProps> = props =>
{
const { itemImage = undefined, itemColor = undefined, itemActive = false, itemCount = 1, itemCountMinimum = 1, itemUniqueSoldout = false, itemUniqueNumber = -2, itemUnseen = false, itemHighlight = false, disabled = false, center = true, column = true, style = {}, classNames = [], position = 'relative', overflow = 'hidden', children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'layout-grid-item', 'border', 'border-2', 'border-muted', 'rounded' ];
if(itemActive) newClassNames.push('!bg-[#ececec] !border-[#fff]');
if(itemUniqueSoldout || (itemUniqueNumber > 0)) newClassNames.push('unique-item');
if(itemUniqueSoldout) newClassNames.push('sold-out');
if(itemUnseen) newClassNames.push('unseen');
if(itemHighlight) newClassNames.push('has-highlight');
if(disabled) newClassNames.push('disabled');
if(itemImage === null) newClassNames.push('icon', 'loading-icon');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ itemActive, itemUniqueSoldout, itemUniqueNumber, itemUnseen, itemHighlight, disabled, itemImage, classNames ]);
const getStyle = useMemo(() =>
{
let newStyle = { ...style };
if(itemImage && !(itemUniqueSoldout || (itemUniqueNumber > 0))) newStyle.backgroundImage = `url(${ itemImage })`;
if(itemColor) newStyle.backgroundColor = itemColor;
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ style, itemImage, itemColor, itemUniqueSoldout, itemUniqueNumber ]);
return (
<Column pointer center={ center } classNames={ getClassNames } column={ column } overflow={ overflow } position={ position } style={ getStyle } { ...rest }>
{ (itemCount > itemCountMinimum) &&
<LayoutItemCountView count={ itemCount } /> }
{ (itemUniqueNumber > 0) &&
<>
<Base fit className="unique-bg-override" style={ { backgroundImage: `url(${ itemImage })` } } />
<div className="absolute bottom-0 unique-item-counter">
<LayoutLimitedEditionStyledNumberView value={ itemUniqueNumber } />
</div>
</> }
{ children }
</Column>
);
};
+13
View File
@@ -0,0 +1,13 @@
import { DetailedHTMLProps, FC, HTMLAttributes } from 'react';
export interface LayoutImageProps extends DetailedHTMLProps<HTMLAttributes<HTMLImageElement>, HTMLImageElement>
{
imageUrl?: string;
}
export const LayoutImage: FC<LayoutImageProps> = props =>
{
const { imageUrl = null, className = '', ...rest } = props;
return <img alt="" className={ 'no-select ' + className } src={ imageUrl } { ...rest } />;
};
+28
View File
@@ -0,0 +1,28 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from '../Base';
interface LayoutItemCountViewProps extends BaseProps<HTMLDivElement>
{
count: number;
}
export const LayoutItemCountView: FC<LayoutItemCountViewProps> = props =>
{
const { count = 0, position = 'absolute', classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'inline-block px-[.65em] py-[.35em] text-[.75em] font-bold leading-none text-[#fff] text-center whitespace-nowrap align-baseline rounded-[.25rem]', '!border-[1px] !border-[solid] !border-[#283F5D]', 'border-black', 'bg-danger', 'px-1', 'top-[2px] right-[2px] text-[9.5px] px-[3px] py-[2px] ' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<Base classNames={ getClassNames } position="absolute" { ...rest }>
{ count }
{ children }
</Base>
);
};
@@ -0,0 +1,15 @@
import { FC } from 'react';
import { Base, BaseProps } from '../Base';
export const LayoutLoadingSpinnerView: FC<BaseProps<HTMLDivElement>> = props =>
{
const { ...rest } = props;
return (
<Base classNames={ [ 'spinner-container' ] } { ...rest } >
<Base className="spinner" />
<Base className="spinner" />
<Base className="spinner" />
</Base>
);
};
@@ -0,0 +1,73 @@
import { GetRoomEngine, NitroRectangle, NitroTexture } from '@nitrots/nitro-renderer';
import { FC, useRef } from 'react';
import { LocalizeText, PlaySound, SoundNames } from '../../api';
import { DraggableWindow } from '../draggable-window';
interface LayoutMiniCameraViewProps {
roomId: number;
textureReceiver: (texture: NitroTexture) => Promise<void>;
onClose: () => void;
}
export const LayoutMiniCameraView: FC<LayoutMiniCameraViewProps> = props => {
const { roomId = -1, textureReceiver = null, onClose = null } = props;
const elementRef = useRef<HTMLDivElement>();
const getCameraBounds = () => {
if (!elementRef || !elementRef.current) return null;
const frameBounds = elementRef.current.getBoundingClientRect();
return new NitroRectangle(
Math.floor(frameBounds.x),
Math.floor(frameBounds.y),
Math.floor(frameBounds.width),
Math.floor(frameBounds.height)
);
};
const takePicture = () => {
PlaySound(SoundNames.CAMERA_SHUTTER);
textureReceiver(GetRoomEngine().createTextureFromRoom(roomId, 1, getCameraBounds()));
};
return (
<DraggableWindow handleSelector=".nitro-room-thumbnail-camera">
<div className="nitro-room-thumbnail-camera px-2">
<div
style={{
position: 'relative',
paddingBottom: '192px', // Matches the space needed to position buttons as per the design
}}
>
<div ref={elementRef} className="camera-frame" />
<div
style={{
position: 'absolute',
bottom: '10px',
left: '10px',
right: '10px',
display: 'flex',
justifyContent: 'space-between',
}}
>
<button
className="btn btn-sm btn-danger"
style={{ width: '80px' }}
onClick={onClose}
>
{LocalizeText('cancel')}
</button>
<button
className="btn btn-sm btn-success"
style={{ width: '80px' }}
onClick={takePicture}
>
{LocalizeText('navigator.thumbeditor.save')}
</button>
</div>
</div>
</div>
</DraggableWindow>
);
};
@@ -0,0 +1,35 @@
import { FC, useMemo } from 'react';
import { NotificationAlertType } from '../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, NitroCardViewProps } from '../card';
export interface LayoutNotificationAlertViewProps extends NitroCardViewProps
{
title?: string;
type?: string;
onClose: () => void;
}
export const LayoutNotificationAlertView: FC<LayoutNotificationAlertViewProps> = props =>
{
const { title = '', onClose = null, classNames = [], children = null,type = NotificationAlertType.DEFAULT, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'nitro-alert' ];
newClassNames.push('nitro-alert-' + type);
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames, type ]);
return (
<NitroCardView classNames={ getClassNames } theme="primary-slim" { ...rest }>
<NitroCardHeaderView headerText={ title } onCloseClick={ onClose } />
<NitroCardContentView grow className="text-black" gap={ 0 } justifyContent="between" overflow="hidden">
{ children }
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,58 @@
import { AnimatePresence, motion } from 'framer-motion';
import { FC, useEffect, useMemo, useState } from 'react';
import { Flex, FlexProps } from '../Flex';
export interface LayoutNotificationBubbleViewProps extends FlexProps
{
fadesOut?: boolean;
timeoutMs?: number;
onClose: () => void;
}
export const LayoutNotificationBubbleView: FC<LayoutNotificationBubbleViewProps> = props =>
{
const { fadesOut = true, timeoutMs = 8000, onClose = null, overflow = 'hidden', classNames = [], ...rest } = props;
const [ isVisible, setIsVisible ] = useState(false);
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'text-sm bg-[#1c1c20f2] px-[5px] py-[6px] [box-shadow:inset_0_5px_#22222799,_inset_0_-4px_#12121599] ', 'rounded' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
useEffect(() =>
{
setIsVisible(true);
return () => setIsVisible(false);
}, []);
useEffect(() =>
{
if(!fadesOut) return;
const timeout = setTimeout(() =>
{
setIsVisible(false);
setTimeout(() => onClose(), 300);
}, timeoutMs);
return () => clearTimeout(timeout);
}, [ fadesOut, timeoutMs, onClose ]);
return (
<AnimatePresence>
{ isVisible &&
<motion.div
initial={ { opacity: 0 }}
animate={ { opacity: 1 }}
exit={ { opacity: 0 }}>
<Flex overflow={ overflow } classNames={ getClassNames } onClick={ onClose } { ...rest } />
</motion.div> }
</AnimatePresence>
);
};
+123
View File
@@ -0,0 +1,123 @@
import { GetRoomEngine, IPetCustomPart, PetFigureData, TextureUtils, Vector3d } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from 'react';
import { Base, BaseProps } from '../Base';
interface LayoutPetImageViewProps extends BaseProps<HTMLDivElement>
{
figure?: string;
typeId?: number;
paletteId?: number;
petColor?: number;
customParts?: IPetCustomPart[];
posture?: string;
headOnly?: boolean;
direction?: number;
scale?: number;
}
export const LayoutPetImageView: FC<LayoutPetImageViewProps> = props =>
{
const { figure = '', typeId = -1, paletteId = -1, petColor = 0xFFFFFF, customParts = [], posture = 'std', headOnly = false, direction = 0, scale = 1, style = {}, ...rest } = props;
const [ petUrl, setPetUrl ] = useState<string>(null);
const [ width, setWidth ] = useState(0);
const [ height, setHeight ] = useState(0);
const isDisposed = useRef(false);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
if(petUrl && petUrl.length) newStyle.backgroundImage = `url(${ petUrl })`;
if(scale !== 1)
{
newStyle.transform = `scale(${ scale })`;
if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
}
newStyle.width = width;
newStyle.height = height;
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ petUrl, scale, style, width, height ]);
useEffect(() =>
{
let url = null;
let petTypeId = typeId;
let petPaletteId = paletteId;
let petColor1 = petColor;
let petCustomParts: IPetCustomPart[] = customParts;
let petHeadOnly = headOnly;
if(figure && figure.length)
{
const petFigureData = new PetFigureData(figure);
petTypeId = petFigureData.typeId;
petPaletteId = petFigureData.paletteId;
petColor1 = petFigureData.color;
petCustomParts = petFigureData.customParts;
}
if(petTypeId === 16) petHeadOnly = false;
const imageResult = GetRoomEngine().getRoomObjectPetImage(petTypeId, petPaletteId, petColor1, new Vector3d((direction * 45)), 64, {
imageReady: async (id, texture, image) =>
{
if(isDisposed.current) return;
if(image)
{
setPetUrl(image.src);
setWidth(image.width);
setHeight(image.height);
}
else if(texture)
{
setPetUrl(await TextureUtils.generateImageUrl(texture));
setWidth(texture.width);
setHeight(texture.height);
}
},
imageFailed: (id) =>
{
}
}, petHeadOnly, 0, petCustomParts, posture);
if(imageResult)
{
(async () =>
{
const image = await imageResult.getImage();
if(image)
{
setPetUrl(image.src);
setWidth(image.width);
setHeight(image.height);
}
})();
}
}, [ figure, typeId, paletteId, petColor, customParts, posture, headOnly, direction ]);
useEffect(() =>
{
isDisposed.current = false;
return () =>
{
isDisposed.current = true;
};
}, []);
const url = `url('${ petUrl }')`;
return <Base classNames={ [ 'pet-image' ] } style={ getStyle } { ...rest } />;
};
@@ -0,0 +1,30 @@
import { FC } from 'react';
import { ProductTypeEnum } from '../../api';
import { LayoutBadgeImageView } from './LayoutBadgeImageView';
import { LayoutCurrencyIcon } from './LayoutCurrencyIcon';
import { LayoutFurniImageView } from './LayoutFurniImageView';
interface LayoutPrizeProductImageViewProps
{
productType: string;
classId: number;
extraParam?: string;
}
export const LayoutPrizeProductImageView: FC<LayoutPrizeProductImageViewProps> = props =>
{
const { productType = ProductTypeEnum.FLOOR, classId = -1, extraParam = undefined } = props;
switch(productType)
{
case ProductTypeEnum.WALL:
case ProductTypeEnum.FLOOR:
return <LayoutFurniImageView productClassId={ classId } productType={ productType } />;
case ProductTypeEnum.BADGE:
return <LayoutBadgeImageView badgeCode={ extraParam }/>;
case ProductTypeEnum.HABBO_CLUB:
return <LayoutCurrencyIcon type="hc" />;
}
return null;
};
+32
View File
@@ -0,0 +1,32 @@
import { FC, useMemo } from 'react';
import { Base, Column, ColumnProps, Flex } from '..';
interface LayoutProgressBarProps extends ColumnProps
{
text?: string;
progress: number;
maxProgress?: number;
}
export const LayoutProgressBar: FC<LayoutProgressBarProps> = props =>
{
const { text = '', progress = 0, maxProgress = 100, position = 'relative', justifyContent = 'center', classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'border-[1px] border-[solid] border-[#fff] p-[2px] h-[20px] rounded-[.25rem] overflow-hidden bg-[#1E7295] ', 'text-white' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<Column classNames={ getClassNames } justifyContent={ justifyContent } position={ position } { ...rest }>
{ text && (text.length > 0) &&
<Flex center fit className="[text-shadow:0px_4px_4px_rgba(0,_0,_0,_.25)] z-20" position="absolute">{ text }</Flex> }
<Base className="h-full z-10 [transition:all_1s] rounded-[.125rem] bg-[repeating-linear-gradient(#2DABC2,_#2DABC2_50%,_#2B91A7_50%,_#2B91A7_100%)]" style={ { width: (~~((((progress - 0) * (100 - 0)) / (maxProgress - 0)) + 0) + '%') } } />
{ children }
</Column>
);
};
@@ -0,0 +1,28 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from '../Base';
interface LayoutRarityLevelViewProps extends BaseProps<HTMLDivElement>
{
level: number;
}
export const LayoutRarityLevelView: FC<LayoutRarityLevelViewProps> = props =>
{
const { level = 0, classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'nitro-rarity-level' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<Base classNames={ getClassNames } { ...rest }>
<div>{ level }</div>
{ children }
</Base>
);
};
@@ -0,0 +1,59 @@
import { GetRoomEngine, TextureUtils, Vector3d } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, useEffect, useMemo, useState } from 'react';
import { Base, BaseProps } from '../Base';
interface LayoutRoomObjectImageViewProps extends BaseProps<HTMLDivElement>
{
roomId: number;
objectId: number;
category: number;
direction?: number;
scale?: number;
}
export const LayoutRoomObjectImageView: FC<LayoutRoomObjectImageViewProps> = props =>
{
const { roomId = -1, objectId = 1, category = -1, direction = 2, scale = 1, style = {}, ...rest } = props;
const [ imageElement, setImageElement ] = useState<HTMLImageElement>(null);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
if(imageElement?.src?.length)
{
newStyle.backgroundImage = `url('${ imageElement.src }')`;
newStyle.width = imageElement.width;
newStyle.height = imageElement.height;
}
if(scale !== 1)
{
newStyle.transform = `scale(${ scale })`;
if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
}
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ imageElement, scale, style ]);
useEffect(() =>
{
const imageResult = GetRoomEngine().getRoomObjectImage(roomId, objectId, category, new Vector3d(direction * 45), 64, {
imageReady: async (id, texture, image) => setImageElement(await TextureUtils.generateImage(texture)),
imageFailed: null
});
// needs (roomObjectImage.data.width > 140) || (roomObjectImage.data.height > 200) scale 1
if(!imageResult) return;
(async () => setImageElement(await TextureUtils.generateImage(imageResult.data)))();
}, [ roomId, objectId, category, direction, scale ]);
if(!imageElement) return null;
return <Base classNames={ [ 'furni-image' ] } style={ getStyle } { ...rest } />;
};
@@ -0,0 +1,89 @@
import { GetRenderer, GetTicker, NitroTicker, RoomPreviewer, TextureUtils } from '@nitrots/nitro-renderer';
import { FC, MouseEvent, useEffect, useRef } from 'react';
export const LayoutRoomPreviewerView: FC<{
roomPreviewer: RoomPreviewer;
height?: number;
}> = props =>
{
const { roomPreviewer = null, height = 0 } = props;
const elementRef = useRef<HTMLDivElement>();
const onClick = (event: MouseEvent<HTMLDivElement>) =>
{
if(!roomPreviewer) return;
if(event.shiftKey) roomPreviewer.changeRoomObjectDirection();
else roomPreviewer.changeRoomObjectState();
};
useEffect(() =>
{
if(!elementRef) return;
const width = elementRef.current.parentElement.clientWidth;
const texture = TextureUtils.createRenderTexture(width, height);
const update = async (ticker: NitroTicker) =>
{
if(!roomPreviewer || !elementRef.current) return;
roomPreviewer.updatePreviewRoomView();
const renderingCanvas = roomPreviewer.getRenderingCanvas();
if(!renderingCanvas.canvasUpdated) return;
GetRenderer().render({
target: texture,
container: renderingCanvas.master,
clear: true
});
let canvas = GetRenderer().texture.generateCanvas(texture);
const base64 = canvas.toDataURL('image/png');
canvas = null;
elementRef.current.style.backgroundImage = `url(${ base64 })`;
};
GetTicker().add(update);
const resizeObserver = new ResizeObserver(() =>
{
if(!roomPreviewer || !elementRef.current) return;
const width = elementRef.current.parentElement.offsetWidth;
roomPreviewer.modifyRoomCanvas(width, height);
update(GetTicker());
});
roomPreviewer.getRoomCanvas(width, height);
resizeObserver.observe(elementRef.current);
return () =>
{
GetTicker().remove(update);
resizeObserver.disconnect();
texture.destroy(true);
};
}, [ roomPreviewer, elementRef, height ]);
return (
<div
ref={ elementRef }
className="relative w-full rounded-md shadow-room-previewer"
style={ {
height,
minHeight: height,
maxHeight: height
} }
onClick={ onClick } />
);
};
@@ -0,0 +1,37 @@
import { FC, useMemo } from 'react';
import { GetConfigurationValue } from '../../api';
import { Base, BaseProps } from '../Base';
export interface LayoutRoomThumbnailViewProps extends BaseProps<HTMLDivElement>
{
roomId?: number;
customUrl?: string;
}
export const LayoutRoomThumbnailView: FC<LayoutRoomThumbnailViewProps> = props =>
{
const { roomId = -1, customUrl = null, shrink = true, overflow = 'hidden', classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'relative w-[110px] h-[110px] bg-[url("@/assets/images/navigator/thumbnail_placeholder.png")] bg-no-repeat bg-center', 'rounded', '!border-[1px] !border-[solid] !border-[#283F5D]' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
const getImageUrl = useMemo(() =>
{
if(customUrl && customUrl.length) return (GetConfigurationValue<string>('image.library.url') + customUrl);
return (GetConfigurationValue<string>('thumbnails.url').replace('%thumbnail%', roomId.toString()));
}, [ customUrl, roomId ]);
return (
<Base classNames={ getClassNames } overflow={ overflow } shrink={ shrink } { ...rest }>
{ getImageUrl && <img alt="" src={ getImageUrl } /> }
{ children }
</Base>
);
};
+42
View File
@@ -0,0 +1,42 @@
import { FC } from 'react';
import { LocalizeText } from '../../api';
import { Base } from '../Base';
import { Column } from '../Column';
import { Flex } from '../Flex';
import { Text } from '../Text';
import { DraggableWindow } from '../draggable-window';
interface LayoutTrophyViewProps
{
color: string;
message: string;
date: string;
senderName: string;
customTitle?: string;
onCloseClick: () => void;
}
export const LayoutTrophyView: FC<LayoutTrophyViewProps> = props =>
{
const { color = '', message = '', date = '', senderName = '', customTitle = null, onCloseClick = null } = props;
return (
<DraggableWindow handleSelector=".drag-handler">
<Column alignItems="center" className={ `nitro-layout-trophy trophy-${ color }` } gap={ 0 }>
<Flex center fullWidth className="trophy-header drag-handler" position="relative">
<Base pointer className="trophy-close" position="absolute" onClick={ onCloseClick } />
<Text bold>{ LocalizeText('widget.furni.trophy.title') }</Text>
</Flex>
<Column className="trophy-content py-1" gap={ 1 }>
{ customTitle &&
<Text bold>{ customTitle }</Text> }
{ message }
</Column>
<Flex alignItems="center" className="trophy-footer mt-1" justifyContent="between">
<Text bold>{ date }</Text>
<Text bold>{ senderName }</Text>
</Flex>
</Column>
</DraggableWindow>
);
};
+29
View File
@@ -0,0 +1,29 @@
import { FC, useMemo } from 'react';
import { GetUserProfile } from '../../api';
import { Base, BaseProps } from '../Base';
export interface UserProfileIconViewProps extends BaseProps<HTMLDivElement>
{
userId?: number;
userName?: string;
}
export const UserProfileIconView: FC<UserProfileIconViewProps> = props =>
{
const { userId = 0, userName = null, classNames = [], pointer = true, children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'bg-[url("@/assets/images/friends/friends-spritesheet.png")]', 'w-[13px] h-[11px] bg-[-51px_-91px]' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<Base classNames={ getClassNames } pointer={ pointer } onClick={ event => GetUserProfile(userId) } { ...rest }>
{ children }
</Base>
);
};
+24
View File
@@ -0,0 +1,24 @@
export * from './LayoutAvatarImageView';
export * from './LayoutBackgroundImage';
export * from './LayoutBadgeImageView';
export * from './LayoutCounterTimeView';
export * from './LayoutCurrencyIcon';
export * from './LayoutFurniIconImageView';
export * from './LayoutFurniImageView';
export * from './LayoutGiftTagView';
export * from './LayoutGridItem';
export * from './LayoutImage';
export * from './LayoutItemCountView';
export * from './LayoutLoadingSpinnerView';
export * from './LayoutMiniCameraView';
export * from './LayoutNotificationAlertView';
export * from './LayoutNotificationBubbleView';
export * from './LayoutPetImageView';
export * from './LayoutProgressBar';
export * from './LayoutRarityLevelView';
export * from './LayoutRoomObjectImageView';
export * from './LayoutRoomPreviewerView';
export * from './LayoutRoomThumbnailView';
export * from './LayoutTrophyView';
export * from './UserProfileIconView';
export * from './limited-edition';
@@ -0,0 +1,35 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from '../../Base';
import { LayoutLimitedEditionStyledNumberView } from './LayoutLimitedEditionStyledNumberView';
interface LayoutLimitedEditionCompactPlateViewProps extends BaseProps<HTMLDivElement>
{
uniqueNumber: number;
uniqueSeries: number;
}
export const LayoutLimitedEditionCompactPlateView: FC<LayoutLimitedEditionCompactPlateViewProps> = props =>
{
const { uniqueNumber = 0, uniqueSeries = 0, classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'unique-compact-plate', 'z-index-1' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<Base classNames={ getClassNames } { ...rest }>
<div>
<LayoutLimitedEditionStyledNumberView value={ uniqueNumber } />
</div>
<div>
<LayoutLimitedEditionStyledNumberView value={ uniqueSeries } />
</div>
{ children }
</Base>
);
};
@@ -0,0 +1,40 @@
import { FC, useMemo } from 'react';
import { LocalizeText } from '../../../api';
import { Base, BaseProps } from '../../Base';
import { Column } from '../../Column';
import { LayoutLimitedEditionStyledNumberView } from './LayoutLimitedEditionStyledNumberView';
interface LayoutLimitedEditionCompletePlateViewProps extends BaseProps<HTMLDivElement>
{
uniqueLimitedItemsLeft: number;
uniqueLimitedSeriesSize: number;
}
export const LayoutLimitedEditionCompletePlateView: FC<LayoutLimitedEditionCompletePlateViewProps> = props =>
{
const { uniqueLimitedItemsLeft = 0, uniqueLimitedSeriesSize = 0, classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'unique-complete-plate' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<Base classNames={ getClassNames } { ...rest }>
<Column className="plate-container" gap={ 0 }>
<div className="flex justify-between items-center">
{ LocalizeText('unique.items.left') }
<div><LayoutLimitedEditionStyledNumberView value={ uniqueLimitedItemsLeft } /></div>
</div>
<div className="flex justify-between items-center">
{ LocalizeText('unique.items.number.sold') }
<div><LayoutLimitedEditionStyledNumberView value={ uniqueLimitedSeriesSize } /></div>
</div>
</Column>
</Base>
);
};
@@ -0,0 +1,18 @@
import { FC } from 'react';
interface LayoutLimitedEditionStyledNumberViewProps
{
value: number;
}
export const LayoutLimitedEditionStyledNumberView: FC<LayoutLimitedEditionStyledNumberViewProps> = props =>
{
const { value = 0 } = props;
const numbers = value.toString().split('');
return (
<>
{ numbers.map((number, index) => <i key={ index } className={ 'limited-edition-number n-' + number } />) }
</>
);
};
@@ -0,0 +1,3 @@
export * from './LayoutLimitedEditionCompactPlateView';
export * from './LayoutLimitedEditionCompletePlateView';
export * from './LayoutLimitedEditionStyledNumberView';