More updates to V2

This commit is contained in:
duckietm 2024-04-25 11:33:15 +02:00
parent 235c2d7bec
commit 2afc5f1143
97 changed files with 4477 additions and 1336 deletions

6
.editorconfig Normal file
View File

@ -0,0 +1,6 @@
[*]
charset = utf-8
insert_final_newline = true
end_of_line = lf
indent_style = space
indent_size = 4

View File

@ -105,6 +105,17 @@
{ {
"prevent": true "prevent": true
} }
],
"react/jsx-sort-props": [
"error",
{
"callbacksLast": true,
"shorthandFirst": true,
"shorthandLast": false,
"ignoreCase": true,
"noSortAlphabetically": false,
"reservedFirst": true
}
] ]
} }
} }

View File

@ -25,6 +25,12 @@
- Update `camera.url, thumbnails.url, url.prefix, habbopages.url` - Update `camera.url, thumbnails.url, url.prefix, habbopages.url`
- You can override any variable by passing it to `NitroConfig` in the index.html - You can override any variable by passing it to `NitroConfig` in the index.html
nitro-renderer>yarn install
\nitro-renderer>yarn link
\nitro-react>yarn install
yarn link "@nitrots/nitro-renderer"
yarn start
## Usage ## Usage
- To use Nitro you need `.nitro` assets generated, see [nitro-converter](https://git.krews.org/nitro/nitro-converter) for instructions - To use Nitro you need `.nitro` assets generated, see [nitro-converter](https://git.krews.org/nitro/nitro-converter) for instructions

View File

@ -0,0 +1,49 @@
const lightenHexColor = (hex, percent) =>
{
// Remove the hash symbol if present
hex = hex.replace(/^#/, '');
// Convert hex to RGB
let r = parseInt(hex.substring(0, 2), 16);
let g = parseInt(hex.substring(2, 4), 16);
let b = parseInt(hex.substring(4, 6), 16);
// Adjust RGB values
r = Math.round(Math.min(255, r + 255 * percent));
g = Math.round(Math.min(255, g + 255 * percent));
b = Math.round(Math.min(255, b + 255 * percent));
// Convert RGB back to hex
const result = ((r << 16) | (g << 8) | b).toString(16);
// Make sure result has 6 digits
return '#' + result.padStart(6, '0');
}
const generateShades = (colors) =>
{
for (let color in colors)
{
let hex = colors[color]
let extended = {}
const shades = [ 50, 100, 200, 300, 400, 500, 600, 700, 900, 950 ];
for (let i = 0; i < shades.length; i++)
{
let shade = shades[i];
extended[shade] = lightenHexColor(hex, shades[(shades.length - 1 - i) ] / 1000);
}
colors[color] = {
DEFAULT: hex,
...extended
}
}
return colors;
}
module.exports = {
generateShades,
lightenHexColor
}

View File

@ -10,7 +10,10 @@
"eslint": "eslint src --ext .ts,.tsx" "eslint": "eslint src --ext .ts,.tsx"
}, },
"dependencies": { "dependencies": {
"@tanstack/react-virtual": "3.0.0-beta.60", "@headlessui/react": "^1.7.18",
"@headlessui/tailwindcss": "^0.2.0",
"@tanstack/react-virtual": "3.2.0",
"dompurify": "^3.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-bootstrap": "^2.2.2", "react-bootstrap": "^2.2.2",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -20,6 +23,7 @@
"use-between": "^1.3.5" "use-between": "^1.3.5"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@types/node": "^20.11.30", "@types/node": "^20.11.30",
"@types/react": "^18.2.67", "@types/react": "^18.2.67",
"@types/react-dom": "^18.2.22", "@types/react-dom": "^18.2.22",
@ -27,13 +31,20 @@
"@typescript-eslint/eslint-plugin": "^7.3.1", "@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1", "@typescript-eslint/parser": "^7.3.1",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.34.1", "eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"postcss": "^8.4.38",
"postcss-nested": "^6.0.1",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.13",
"sass": "^1.72.0", "sass": "^1.72.0",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.2", "typescript": "^5.4.2",
"vite": "^5.1.6" "vite": "^5.1.6",
"vite-tsconfig-paths": "^4.3.2"
} }
} }

8
postcss.config.js Normal file
View File

@ -0,0 +1,8 @@
/** @type {import("postcss-load-config").Config} */
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}

7
prettier.config.js Normal file
View File

@ -0,0 +1,7 @@
/** @type {import("prettier").Config} */
module.exports = {
plugins: [
'prettier-plugin-tailwindcss'
]
}

View File

@ -1,8 +1,8 @@
{ {
"socket.url": "ws://localhost:2096", "socket.url": "ws://localhost:2096",
"asset.url": "http://localhost/gamedata", "asset.url": "http://192.168.0.8/gamedata",
"image.library.url": "http://localhost/gamedata/c_images/", "image.library.url": "http://192.168.0.8/gamedata/c_images/",
"hof.furni.url": "http://localhost/gamedata", "hof.furni.url": "http://192.168.0.8/gamedata",
"images.url": "${asset.url}/images", "images.url": "${asset.url}/images",
"gamedata.url": "${asset.url}", "gamedata.url": "${asset.url}",
"sounds.url": "${asset.url}/sounds/%sample%.mp3", "sounds.url": "${asset.url}/sounds/%sample%.mp3",
@ -23,12 +23,12 @@
"badge.asset.url": "${image.library.url}album1584/%badgename%.gif", "badge.asset.url": "${image.library.url}album1584/%badgename%.gif",
"furni.rotation.bounce.steps": 20, "furni.rotation.bounce.steps": 20,
"furni.rotation.bounce.height": 0.0625, "furni.rotation.bounce.height": 0.0625,
"enable.avatar.arrow": false, "enable.avatar.arrow": true,
"system.log.debug": false, "system.log.debug": true,
"system.log.warn": false, "system.log.warn": true,
"system.log.error": false, "system.log.error": true,
"system.log.events": false, "system.log.events": false,
"system.log.packets": false, "system.log.packets": true,
"system.fps.animation": 24, "system.fps.animation": 24,
"system.fps.max": 60, "system.fps.max": 60,
"system.pong.manually": true, "system.pong.manually": true,

View File

@ -1,7 +1,7 @@
{ {
"image.library.notifications.url": "${image.library.url}notifications/%image%.png", "image.library.notifications.url": "${image.library.url}notifications/%image%.png",
"achievements.images.url": "${image.library.url}Quests/%image%.png", "achievements.images.url": "${image.library.url}Quests/%image%.png",
"camera.url": "http://localhost/camera/photo", "camera.url": "http://192.168.0.8/camera/photo",
"thumbnails.url": "/camera/photo/thumb/%thumbnail%.png", "thumbnails.url": "/camera/photo/thumb/%thumbnail%.png",
"url.prefix": "", "url.prefix": "",
"habbopages.url": "${url.prefix}/", "habbopages.url": "${url.prefix}/",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -114,3 +114,25 @@
.btn-sm { .btn-sm {
@include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm); @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm);
} }
// Tool-Bar button left side
.btn-toggle {
border-image-source: url("@/assets/images/buttons/toggle_bg.png");
border-image-slice: 6 6 6 6 fill;
border-image-width: 6px 6px 6px 6px;
cursor: pointer;
.toggle-icon {
background-repeat: no-repeat;
width: 6px;
height: 8px;
&.left {
background-image: url("@/assets/images/buttons/toggle_left.png");
}
&.right {
background-image: url("@/assets/images/buttons/toggle_right.png");
}
}
}

View File

@ -0,0 +1,33 @@
import { FC, useMemo } from 'react';
import { NotificationAlertType } from '../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, NitroCardViewProps } from '../card';
export interface LayoutNotificationCreditsProps extends NitroCardViewProps
{
title?: string;
type?: string;
onClose: () => void;
}
export const LayoutNotificationCredits : FC<LayoutNotificationCreditsProps> = props =>
{
const { title = '', onClose = null, classNames = [], children = null,type = NotificationAlertType.DEFAULT, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = ['nitro-alert', 'nitro-alert-credits'];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames, type ]);
return (
<NitroCardView classNames={ getClassNames } theme="primary" { ...rest }>
<NitroCardHeaderView headerText={ title } onCloseClick={ onClose } />
<NitroCardContentView grow justifyContent="between" overflow="hidden" className="text-black" gap={ 0 }>
{ children }
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -12,6 +12,7 @@ export * from './LayoutItemCountView';
export * from './LayoutLoadingSpinnerView'; export * from './LayoutLoadingSpinnerView';
export * from './LayoutMiniCameraView'; export * from './LayoutMiniCameraView';
export * from './LayoutNotificationAlertView'; export * from './LayoutNotificationAlertView';
export * from './LayoutNotificationCredits';
export * from './LayoutNotificationBubbleView'; export * from './LayoutNotificationBubbleView';
export * from './LayoutPetImageView'; export * from './LayoutPetImageView';
export * from './LayoutProgressBar'; export * from './LayoutProgressBar';

View File

@ -1,336 +0,0 @@
.nitro-avatar-editor-spritesheet {
background: url('@/assets/images/avatareditor/avatar-editor-spritesheet.png') transparent no-repeat;
&.arrow-left-icon {
width: 28px;
height: 21px;
background-position: -226px -131px;
}
&.arrow-right-icon {
width: 28px;
height: 21px;
background-position: -226px -162px;
}
&.ca-icon {
width: 25px;
height: 25px;
background-position: -226px -61px;
&.selected {
width: 25px;
height: 25px;
background-position: -226px -96px;
}
}
&.cc-icon {
width: 31px;
height: 29px;
background-position: -145px -5px;
&.selected {
width: 31px;
height: 29px;
background-position: -145px -44px;
}
}
&.ch-icon {
width: 29px;
height: 24px;
background-position: -186px -39px;
&.selected {
width: 29px;
height: 24px;
background-position: -186px -73px;
}
}
&.clear-icon {
width: 27px;
height: 27px;
background-position: -145px -157px;
}
&.cp-icon {
width: 30px;
height: 24px;
background-position: -145px -264px;
&.selected {
width: 30px;
height: 24px;
background-position: -186px -5px;
}
}
&.ea-icon {
width: 35px;
height: 16px;
background-position: -226px -193px;
&.selected {
width: 35px;
height: 16px;
background-position: -226px -219px;
}
}
&.fa-icon {
width: 27px;
height: 20px;
background-position: -186px -137px;
&.selected {
width: 27px;
height: 20px;
background-position: -186px -107px;
}
}
&.female-icon {
width: 18px;
height: 27px;
background-position: -186px -202px;
&.selected {
width: 18px;
height: 27px;
background-position: -186px -239px;
}
}
&.ha-icon {
width: 25px;
height: 22px;
background-position: -226px -245px;
&.selected {
width: 25px;
height: 22px;
background-position: -226px -277px;
}
}
&.he-icon {
width: 31px;
height: 27px;
background-position: -145px -83px;
&.selected {
width: 31px;
height: 27px;
background-position: -145px -120px;
}
}
&.hr-icon {
width: 29px;
height: 25px;
background-position: -145px -194px;
&.selected {
width: 29px;
height: 25px;
background-position: -145px -229px;
}
}
&.lg-icon {
width: 19px;
height: 20px;
background-position: -303px -45px;
&.selected {
width: 19px;
height: 20px;
background-position: -303px -75px;
}
}
&.loading-icon {
width: 21px;
height: 25px;
background-position: -186px -167px;
}
&.male-icon {
width: 21px;
height: 21px;
background-position: -186px -276px;
&.selected {
width: 21px;
height: 21px;
background-position: -272px -5px;
}
}
&.sellable-icon {
width: 17px;
height: 15px;
background-position: -303px -105px;
}
&.sh-icon {
width: 37px;
height: 10px;
background-position: -303px -5px;
&.selected {
width: 37px;
height: 10px;
background-position: -303px -25px;
}
}
&.spotlight-icon {
width: 130px;
height: 305px;
background-position: -5px -5px;
}
&.wa-icon {
width: 36px;
height: 18px;
background-position: -226px -5px;
&.selected {
width: 36px;
height: 18px;
background-position: -226px -33px;
}
}
}
.nitro-avatar-editor-wardrobe-figure-preview {
background-color: $pale-sky;
overflow: hidden;
z-index: 1;
.avatar-image {
position: absolute;
bottom: -15px;
margin: 0 auto;
z-index: 4;
}
.avatar-shadow {
position: absolute;
left: 0;
right: 0;
bottom: 25px;
width: 40px;
height: 20px;
margin: 0 auto;
border-radius: 100%;
background-color: rgba(0, 0, 0, 0.20);
z-index: 2;
}
&:after {
position: absolute;
content: '';
top: 75%;
bottom: 0;
left: 0;
right: 0;
border-radius: 50%;
background-color: $pale-sky;
box-shadow: 0 0 8px 2px rgba($white,.6);
transform: scale(2);
}
.button-container {
position: absolute;
bottom: 0;
z-index: 5;
}
}
.nitro-avatar-editor {
width: $avatar-editor-width;
height: $avatar-editor-height;
.category-item {
height: 40px;
}
.figure-preview-container {
position: relative;
height: 100%;
background-color: $pale-sky;
overflow: hidden;
z-index: 1;
.arrow-container {
position: absolute;
width: 100%;
margin: 0 auto;
padding: 0 10px;
display: flex;
justify-content: space-between;
bottom: 12px;
z-index: 5;
.icon {
cursor: pointer;
}
}
.avatar-image {
position: absolute;
left: 0;
right: 0;
bottom: 50px;
margin: 0 auto;
z-index: 4;
}
.avatar-spotlight {
position: absolute;
top: -10px;
left: 0;
right: 0;
margin: 0 auto;
opacity: 0.3;
pointer-events: none;
z-index: 3;
}
.avatar-shadow {
position: absolute;
left: 0;
right: 0;
bottom: 15px;
width: 70px;
height: 30px;
margin: 0 auto;
border-radius: 100%;
background-color: rgba(0, 0, 0, 0.20);
z-index: 2;
}
&:after {
position: absolute;
content: '';
top: 75%;
bottom: 0;
left: 0;
right: 0;
border-radius: 50%;
background-color: $pale-sky;
box-shadow: 0 0 8px 2px rgba($white,.6);
transform: scale(2);
}
}
}

View File

@ -1,114 +0,0 @@
import { AddLinkEventTracker, AvatarEditorFigureCategory, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { FaDice, FaTrash, FaUndo } from 'react-icons/fa';
import { AvatarEditorAction, LocalizeText } from '../../api';
import { Button, ButtonGroup, Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
import { useAvatarEditor } from '../../hooks';
import { AvatarEditorModelView } from './views/AvatarEditorModelView';
const DEFAULT_MALE_FIGURE: string = 'hr-100.hd-180-7.ch-215-66.lg-270-79.sh-305-62.ha-1002-70.wa-2007';
const DEFAULT_FEMALE_FIGURE: string = 'hr-515-33.hd-600-1.ch-635-70.lg-716-66-62.sh-735-68';
export const AvatarEditorNewView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const { setIsVisible: setEditorVisibility, avatarModels, activeModelKey, setActiveModelKey } = useAvatarEditor();
const processAction = (action: string) =>
{
switch(action)
{
case AvatarEditorAction.ACTION_CLEAR:
return;
case AvatarEditorAction.ACTION_RESET:
return;
case AvatarEditorAction.ACTION_RANDOMIZE:
return;
case AvatarEditorAction.ACTION_SAVE:
return;
}
}
useEffect(() =>
{
const linkTracker: ILinkEventTracker = {
linkReceived: (url: string) =>
{
const parts = url.split('/');
if(parts.length < 2) return;
switch(parts[1])
{
case 'show':
setIsVisible(true);
return;
case 'hide':
setIsVisible(false);
return;
case 'toggle':
setIsVisible(prevValue => !prevValue);
return;
}
},
eventUrlPrefix: 'avatar-editor/'
};
AddLinkEventTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, []);
useEffect(() =>
{
setEditorVisibility(isVisible)
}, [ isVisible, setEditorVisibility ]);
if(!isVisible) return null;
return (
<NitroCardView uniqueKey="avatar-editor" className="nitro-avatar-editor">
<NitroCardHeaderView headerText={ LocalizeText('avatareditor.title') } onCloseClick={ event => setIsVisible(false) } />
<NitroCardTabsView>
{ Object.keys(avatarModels).map(modelKey =>
{
const isActive = (activeModelKey === modelKey);
return (
<NitroCardTabsItemView key={ modelKey } isActive={ isActive } onClick={ event => setActiveModelKey(modelKey) }>
{ LocalizeText(`avatareditor.category.${ modelKey }`) }
</NitroCardTabsItemView>
);
}) }
</NitroCardTabsView>
<NitroCardContentView>
<Grid>
<Column size={ 9 } overflow="hidden">
{ ((activeModelKey.length > 0) && (activeModelKey !== AvatarEditorFigureCategory.WARDROBE)) &&
<AvatarEditorModelView name={ activeModelKey } categories={ avatarModels[activeModelKey] } /> }
{ (activeModelKey === AvatarEditorFigureCategory.WARDROBE) }
</Column>
<Column size={ 3 } overflow="hidden">
{ /* <AvatarEditorFigurePreviewView figureData={ figureData } /> */ }
<Column grow gap={ 1 }>
<ButtonGroup>
<Button variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_RESET) }>
<FaUndo className="fa-icon" />
</Button>
<Button variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_CLEAR) }>
<FaTrash className="fa-icon" />
</Button>
<Button variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_RANDOMIZE) }>
<FaDice className="fa-icon" />
</Button>
</ButtonGroup>
<Button className="w-100" variant="success" onClick={ event => processAction(AvatarEditorAction.ACTION_SAVE) }>
{ LocalizeText('avatareditor.save') }
</Button>
</Column>
</Column>
</Grid>
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -1,30 +0,0 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from '../../../common';
type AvatarIconType = 'male' | 'female' | 'clear' | 'sellable' | string;
export interface AvatarEditorIconProps extends BaseProps<HTMLDivElement>
{
icon: AvatarIconType;
selected?: boolean;
}
export const AvatarEditorIcon: FC<AvatarEditorIconProps> = props =>
{
const { icon = null, selected = false, classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'nitro-avatar-editor-spritesheet' ];
if(icon && icon.length) newClassNames.push(icon + '-icon');
if(selected) newClassNames.push('selected');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ icon, selected, classNames ]);
return <Base classNames={ getClassNames } { ...rest } />
}

View File

@ -1,85 +0,0 @@
import { AvatarEditorFigureCategory } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { FigureData, IAvatarEditorCategory } from '../../../api';
import { Column, Flex, Grid } from '../../../common';
import { useAvatarEditor } from '../../../hooks';
import { AvatarEditorIcon } from './AvatarEditorIcon';
import { AvatarEditorFigureSetView } from './figure-set';
import { AvatarEditorPaletteSetView } from './palette-set';
export const AvatarEditorModelView: FC<{
name: string,
categories: IAvatarEditorCategory[]
}> = props =>
{
const { name = '', categories = [] } = props;
const [ activeSetType, setActiveSetType ] = useState<string>('');
const { maxPaletteCount = 1 } = useAvatarEditor();
const activeCategory = useMemo(() =>
{
return categories.find(category => category.setType === activeSetType) ?? null;
}, [ categories, activeSetType ]);
const setGender = (gender: string) =>
{
//
}
useEffect(() =>
{
if(!activeCategory) return;
// we need to run this when we change which parts r selected
/* for(const partItem of activeCategory.partItems)
{
if(!partItem || !part.isSelected) continue;
setMaxPaletteCount(part.maxColorIndex || 1);
break;
} */
}, [ activeCategory ])
useEffect(() =>
{
if(!categories || !categories.length) return;
setActiveSetType(categories[0]?.setType)
}, [ categories ]);
if(!activeCategory) return null;
return (
<Grid>
<Column size={ 2 }>
{ (name === AvatarEditorFigureCategory.GENERIC) &&
<>
<Flex center pointer className="category-item" onClick={ event => setGender(FigureData.MALE) }>
<AvatarEditorIcon icon="male" selected={ false } />
</Flex>
<Flex center pointer className="category-item" onClick={ event => setGender(FigureData.FEMALE) }>
<AvatarEditorIcon icon="female" selected={ false } />
</Flex>
</> }
{ (name !== AvatarEditorFigureCategory.GENERIC) && (categories.length > 0) && categories.map(category =>
{
return (
<Flex center pointer key={ category.setType } className="category-item" onClick={ event => setActiveSetType(category.setType) }>
<AvatarEditorIcon icon={ category.setType } selected={ (activeSetType === category.setType) } />
</Flex>
);
}) }
</Column>
<Column size={ 5 } overflow="hidden">
<AvatarEditorFigureSetView category={ activeCategory } />
</Column>
<Column size={ 5 } overflow="hidden">
{ (maxPaletteCount >= 1) &&
<AvatarEditorPaletteSetView category={ activeCategory } paletteIndex={ 0 } /> }
{ (maxPaletteCount === 2) &&
<AvatarEditorPaletteSetView category={ activeCategory } paletteIndex={ 1 } /> }
</Column>
</Grid>
);
}

View File

@ -1,53 +0,0 @@
import { FC, useEffect, useState } from 'react';
import { AvatarEditorThumbnailsHelper, FigureData, GetConfigurationValue, IAvatarEditorCategoryPartItem } from '../../../../api';
import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../../common';
import { useAvatarEditor } from '../../../../hooks';
import { AvatarEditorIcon } from '../AvatarEditorIcon';
export const AvatarEditorFigureSetItemView: FC<{
setType: string;
partItem: IAvatarEditorCategoryPartItem;
isSelected: boolean;
} & LayoutGridItemProps> = props =>
{
const { setType = null, partItem = null, isSelected = false, ...rest } = props;
const [ assetUrl, setAssetUrl ] = useState<string>('');
const { selectedColorParts = null, getFigureStringWithFace = null } = useAvatarEditor();
const isHC = !GetConfigurationValue<boolean>('hc.disabled', false) && ((partItem.partSet?.clubLevel ?? 0) > 0);
useEffect(() =>
{
if(!setType || !setType.length || !partItem) return;
const loadImage = async () =>
{
const isHC = !GetConfigurationValue<boolean>('hc.disabled', false) && ((partItem.partSet?.clubLevel ?? 0) > 0);
let url: string = null;
if(setType === FigureData.FACE)
{
url = await AvatarEditorThumbnailsHelper.buildForFace(getFigureStringWithFace(partItem.id), isHC);
}
else
{
url = await AvatarEditorThumbnailsHelper.build(setType, partItem, partItem.usesColor, selectedColorParts[setType] ?? null, isHC);
}
if(url && url.length) setAssetUrl(url);
}
loadImage();
}, [ setType, partItem, selectedColorParts, getFigureStringWithFace ]);
if(!partItem) return null;
return (
<LayoutGridItem itemImage={ (partItem.isClear ? undefined : assetUrl) } itemActive={ isSelected } style={ { width: '100%', 'flex': '1' } } { ...rest }>
{ !partItem.isClear && isHC && <LayoutCurrencyIcon className="position-absolute end-1 bottom-1" type="hc" /> }
{ partItem.isClear && <AvatarEditorIcon icon="clear" /> }
{ !partItem.isClear && partItem.partSet.isSellable && <AvatarEditorIcon icon="sellable" position="absolute" className="end-1 bottom-1" /> }
</LayoutGridItem>
);
}

View File

@ -1,36 +0,0 @@
import { FC, useRef } from 'react';
import { IAvatarEditorCategory, IAvatarEditorCategoryPartItem } from '../../../../api';
import { InfiniteGrid } from '../../../../common';
import { useAvatarEditor } from '../../../../hooks';
import { AvatarEditorFigureSetItemView } from './AvatarEditorFigureSetItemView';
export const AvatarEditorFigureSetView: FC<{
category: IAvatarEditorCategory
}> = props =>
{
const { category = null } = props;
const { selectedParts = null, selectEditorPart } = useAvatarEditor();
const elementRef = useRef<HTMLDivElement>(null);
const isPartItemSelected = (partItem: IAvatarEditorCategoryPartItem) =>
{
if(!category || !category.setType || !selectedParts || !selectedParts[category.setType]) return false;
const partId = selectedParts[category.setType];
return (partId === partItem.id);
}
const columnCount = 3;
return (
<InfiniteGrid rows={ category.partItems } columnCount={ columnCount } overscan={ 5 } itemRender={ (item: IAvatarEditorCategoryPartItem) =>
{
if(!item) return null;
return (
<AvatarEditorFigureSetItemView key={ item.id } setType={ category.setType } partItem={ item } isSelected={ isPartItemSelected(item) } onClick={ event => selectEditorPart(category.setType, item.partSet?.id ?? -1) } />
)
} } />
);
}

View File

@ -1,2 +0,0 @@
export * from './AvatarEditorFigureSetItemView';
export * from './AvatarEditorFigureSetView';

View File

@ -1,27 +0,0 @@
import { ColorConverter, IPartColor } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { GetConfigurationValue } from '../../../../api';
import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../../common';
export interface AvatarEditorPaletteSetItemProps extends LayoutGridItemProps
{
setType: string;
partColor: IPartColor;
isSelected: boolean;
}
// its disabled if its hc and you dont have it
export const AvatarEditorPaletteSetItem: FC<AvatarEditorPaletteSetItemProps> = props =>
{
const { setType = null, partColor = null, isSelected = false, ...rest } = props;
if(!partColor) return null;
const isHC = !GetConfigurationValue<boolean>('hc.disabled', false) && (partColor.clubLevel > 0);
return (
<LayoutGridItem itemHighlight itemColor={ ColorConverter.int2rgb(partColor.rgb) } itemActive={ isSelected } className="clear-bg" { ...rest }>
{ isHC && <LayoutCurrencyIcon className="position-absolute end-1 bottom-1" type="hc" /> }
</LayoutGridItem>
);
}

View File

@ -1,33 +0,0 @@
import { IPartColor } from '@nitrots/nitro-renderer';
import { FC, useRef } from 'react';
import { IAvatarEditorCategory } from '../../../../api';
import { AutoGrid } from '../../../../common';
import { useAvatarEditor } from '../../../../hooks';
import { AvatarEditorPaletteSetItem } from './AvatarEditorPaletteSetItemView';
export const AvatarEditorPaletteSetView: FC<{
category: IAvatarEditorCategory,
paletteIndex: number;
}> = props =>
{
const { category = null, paletteIndex = -1 } = props;
const paletteSet = category?.colorItems[paletteIndex] ?? null;
const { selectedColors = null, selectEditorColor } = useAvatarEditor();
const elementRef = useRef<HTMLDivElement>(null);
const isPartColorSelected = (partColor: IPartColor) =>
{
if(!category || !category.setType || !selectedColors || !selectedColors[category.setType] || !selectedColors[category.setType][paletteIndex]) return false;
const colorId = selectedColors[category.setType][paletteIndex];
return (colorId === partColor.id);
}
return (
<AutoGrid innerRef={ elementRef } gap={ 1 } columnCount={ 5 } columnMinWidth={ 30 }>
{ (paletteSet.length > 0) && paletteSet.map(item =>
<AvatarEditorPaletteSetItem key={ item.id } setType={ category.setType } partColor={ item } isSelected={ isPartColorSelected(item) } onClick={ event => selectEditorColor(category.setType, paletteIndex, item.id) } />) }
</AutoGrid>
);
}

View File

@ -1,2 +0,0 @@
export * from './AvatarEditorPaletteSetItemView';
export * from './AvatarEditorPaletteSetView';

View File

@ -258,7 +258,7 @@
} }
.nitro-avatar-editor { .nitro-avatar-editor {
width: $avatar-editor-width; width: $avatar-editor-width + 50px;
height: $avatar-editor-height; height: $avatar-editor-height;
.category-item { .category-item {

View File

@ -1,5 +1,7 @@
import { GetAvatarRenderManager, IAvatarFigureContainer, SaveWardrobeOutfitMessageComposer } from '@nitrots/nitro-renderer'; import { GetAvatarRenderManager, IAvatarFigureContainer, SaveWardrobeOutfitMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback } from 'react'; import { FC, useCallback } from 'react';
import { FaSave } from 'react-icons/fa';
import { GiClothes } from 'react-icons/gi';
import { GetClubMemberLevel, GetConfigurationValue, LocalizeText, SendMessageComposer } from '../../api'; import { GetClubMemberLevel, GetConfigurationValue, LocalizeText, SendMessageComposer } from '../../api';
import { Base, Button, Flex, InfiniteGrid, LayoutAvatarImageView, LayoutCurrencyIcon, LayoutGridItem } from '../../common'; import { Base, Button, Flex, InfiniteGrid, LayoutAvatarImageView, LayoutCurrencyIcon, LayoutGridItem } from '../../common';
import { useAvatarEditor } from '../../hooks'; import { useAvatarEditor } from '../../hooks';
@ -48,10 +50,9 @@ export const AvatarEditorWardrobeView: FC<{}> = props =>
<LayoutAvatarImageView figure={ figureContainer.getFigureString() } gender={ gender } direction={ 2 } /> } <LayoutAvatarImageView figure={ figureContainer.getFigureString() } gender={ gender } direction={ 2 } /> }
<Base className="avatar-shadow" /> <Base className="avatar-shadow" />
{ !hcDisabled && (clubLevel > 0) && <LayoutCurrencyIcon className="position-absolute top-1 start-1" type="hc" /> } { !hcDisabled && (clubLevel > 0) && <LayoutCurrencyIcon className="position-absolute top-1 start-1" type="hc" /> }
<Flex gap={ 1 } className="button-container"> <Flex gap={1} className="button-container">
<Button variant="link" fullWidth onClick={ event => saveFigureAtWardrobeIndex(index) }>{ LocalizeText('avatareditor.wardrobe.save') }</Button> <Button variant="link" fullWidth onClick={() => saveFigureAtWardrobeIndex(index)} title={LocalizeText('avatareditor.wardrobe.save')}> <FaSave size={20} style={{ color: 'black' }} /> </Button>
{ figureContainer && {figureContainer && <Button variant="link" fullWidth onClick={event => wearFigureAtIndex(index)} disabled={(clubLevel > GetClubMemberLevel())} title={LocalizeText('widget.generic_usable.button.use')}> <GiClothes size={20} style={{ color: 'white' }} /> </Button>}
<Button variant="link" fullWidth onClick={ event => wearFigureAtIndex(index) } disabled={ (clubLevel > GetClubMemberLevel()) }>{ LocalizeText('widget.generic_usable.button.use') }</Button> }
</Flex> </Flex>
</LayoutGridItem> </LayoutGridItem>
) )

View File

@ -1,55 +0,0 @@
import { AvatarDirectionAngle } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { FigureData } from '../../../api';
import { Base, Column, LayoutAvatarImageView } from '../../../common';
import { AvatarEditorIcon } from './AvatarEditorIcon';
export interface AvatarEditorFigurePreviewViewProps
{
figureData: FigureData;
}
export const AvatarEditorFigurePreviewView: FC<AvatarEditorFigurePreviewViewProps> = props =>
{
const { figureData = null } = props;
const [ updateId, setUpdateId ] = useState(-1);
const rotateFigure = (direction: number) =>
{
if(direction < AvatarDirectionAngle.MIN_DIRECTION)
{
direction = (AvatarDirectionAngle.MAX_DIRECTION + (direction + 1));
}
if(direction > AvatarDirectionAngle.MAX_DIRECTION)
{
direction = (direction - (AvatarDirectionAngle.MAX_DIRECTION + 1));
}
figureData.direction = direction;
}
useEffect(() =>
{
if(!figureData) return;
figureData.notify = () => setUpdateId(prevValue => (prevValue + 1));
return () =>
{
figureData.notify = null;
}
}, [ figureData ] );
return (
<Column className="figure-preview-container" overflow="hidden" position="relative">
<LayoutAvatarImageView figure={ figureData.getFigureString() } direction={ figureData.direction } scale={ 2 } />
<AvatarEditorIcon className="avatar-spotlight" icon="spotlight" />
<Base className="avatar-shadow" />
<Base className="arrow-container">
<AvatarEditorIcon pointer icon="arrow-left" onClick={ event => rotateFigure(figureData.direction + 1) } />
<AvatarEditorIcon pointer icon="arrow-right" onClick={ event => rotateFigure(figureData.direction - 1) } />
</Base>
</Column>
);
}

View File

@ -1,30 +0,0 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from '../../../common';
type AvatarIconType = 'male' | 'female' | 'clear' | 'sellable' | string;
export interface AvatarEditorIconProps extends BaseProps<HTMLDivElement>
{
icon: AvatarIconType;
selected?: boolean;
}
export const AvatarEditorIcon: FC<AvatarEditorIconProps> = props =>
{
const { icon = null, selected = false, classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'nitro-avatar-editor-spritesheet' ];
if(icon && icon.length) newClassNames.push(icon + '-icon');
if(selected) newClassNames.push('selected');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ icon, selected, classNames ]);
return <Base classNames={ getClassNames } { ...rest } />
}

View File

@ -1,88 +0,0 @@
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useState } from 'react';
import { CategoryData, FigureData, IAvatarEditorCategoryModel } from '../../../api';
import { Column, Flex, Grid } from '../../../common';
import { AvatarEditorIcon } from './AvatarEditorIcon';
import { AvatarEditorFigureSetView } from './figure-set/AvatarEditorFigureSetView';
import { AvatarEditorPaletteSetView } from './palette-set/AvatarEditorPaletteSetView';
export interface AvatarEditorModelViewProps
{
model: IAvatarEditorCategoryModel;
gender: string;
setGender: Dispatch<SetStateAction<string>>;
}
export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
{
const { model = null, gender = null, setGender = null } = props;
const [ activeCategory, setActiveCategory ] = useState<CategoryData>(null);
const [ maxPaletteCount, setMaxPaletteCount ] = useState(1);
const selectCategory = useCallback((name: string) =>
{
const category = model.categories.get(name);
if(!category) return;
category.init();
setActiveCategory(category);
for(const part of category.parts)
{
if(!part || !part.isSelected) continue;
setMaxPaletteCount(part.maxColorIndex || 1);
break;
}
}, [ model ]);
useEffect(() =>
{
model.init();
for(const name of model.categories.keys())
{
selectCategory(name);
break;
}
}, [ model, selectCategory ]);
if(!model || !activeCategory) return null;
return (
<Grid>
<Column size={ 2 }>
{ model.canSetGender &&
<>
<Flex center pointer className="category-item" onClick={ event => setGender(FigureData.MALE) }>
<AvatarEditorIcon icon="male" selected={ (gender === FigureData.MALE) } />
</Flex>
<Flex center pointer className="category-item" onClick={ event => setGender(FigureData.FEMALE) }>
<AvatarEditorIcon icon="female" selected={ (gender === FigureData.FEMALE) } />
</Flex>
</> }
{ !model.canSetGender && model.categories && (model.categories.size > 0) && Array.from(model.categories.keys()).map(name =>
{
const category = model.categories.get(name);
return (
<Flex center pointer key={ name } className="category-item" onClick={ event => selectCategory(name) }>
<AvatarEditorIcon icon={ category.name } selected={ (activeCategory === category) } />
</Flex>
);
}) }
</Column>
<Column size={ 5 } overflow="hidden">
<AvatarEditorFigureSetView model={ model } category={ activeCategory } setMaxPaletteCount={ setMaxPaletteCount } />
</Column>
<Column size={ 5 } overflow="hidden">
{ (maxPaletteCount >= 1) &&
<AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(0) } paletteIndex={ 0 } /> }
{ (maxPaletteCount === 2) &&
<AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(1) } paletteIndex={ 1 } /> }
</Column>
</Grid>
);
}

View File

@ -1,79 +0,0 @@
import { GetAvatarRenderManager, IAvatarFigureContainer, SaveWardrobeOutfitMessageComposer } from '@nitrots/nitro-renderer';
import { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react';
import { FigureData, GetClubMemberLevel, GetConfigurationValue, LocalizeText, SendMessageComposer } from '../../../api';
import { AutoGrid, Base, Button, Flex, LayoutAvatarImageView, LayoutCurrencyIcon, LayoutGridItem } from '../../../common';
export interface AvatarEditorWardrobeViewProps
{
figureData: FigureData;
savedFigures: [ IAvatarFigureContainer, string ][];
setSavedFigures: Dispatch<SetStateAction<[ IAvatarFigureContainer, string][]>>;
loadAvatarInEditor: (figure: string, gender: string, reset?: boolean) => void;
}
export const AvatarEditorWardrobeView: FC<AvatarEditorWardrobeViewProps> = props =>
{
const { figureData = null, savedFigures = [], setSavedFigures = null, loadAvatarInEditor = null } = props;
const hcDisabled = GetConfigurationValue<boolean>('hc.disabled', false);
const wearFigureAtIndex = useCallback((index: number) =>
{
if((index >= savedFigures.length) || (index < 0)) return;
const [ figure, gender ] = savedFigures[index];
loadAvatarInEditor(figure.getFigureString(), gender);
}, [ savedFigures, loadAvatarInEditor ]);
const saveFigureAtWardrobeIndex = useCallback((index: number) =>
{
if(!figureData || (index >= savedFigures.length) || (index < 0)) return;
const newFigures = [ ...savedFigures ];
const figure = figureData.getFigureString();
const gender = figureData.gender;
newFigures[index] = [ GetAvatarRenderManager().createFigureContainer(figure), gender ];
setSavedFigures(newFigures);
SendMessageComposer(new SaveWardrobeOutfitMessageComposer((index + 1), figure, gender));
}, [ figureData, savedFigures, setSavedFigures ]);
const figures = useMemo(() =>
{
if(!savedFigures || !savedFigures.length) return [];
const items: JSX.Element[] = [];
savedFigures.forEach(([ figureContainer, gender ], index) =>
{
let clubLevel = 0;
if(figureContainer) clubLevel = GetAvatarRenderManager().getFigureClubLevel(figureContainer, gender);
items.push(
<LayoutGridItem key={ index } position="relative" overflow="hidden" className="nitro-avatar-editor-wardrobe-figure-preview">
{ figureContainer &&
<LayoutAvatarImageView figure={ figureContainer.getFigureString() } gender={ gender } direction={ 2 } /> }
<Base className="avatar-shadow" />
{ !hcDisabled && (clubLevel > 0) && <LayoutCurrencyIcon className="position-absolute top-1 start-1" type="hc" /> }
<Flex gap={ 1 } className="button-container">
<Button variant="link" fullWidth onClick={ event => saveFigureAtWardrobeIndex(index) }>{ LocalizeText('avatareditor.wardrobe.save') }</Button>
{ figureContainer &&
<Button variant="link" fullWidth onClick={ event => wearFigureAtIndex(index) } disabled={ (clubLevel > GetClubMemberLevel()) }>{ LocalizeText('widget.generic_usable.button.use') }</Button> }
</Flex>
</LayoutGridItem>
);
});
return items;
}, [ savedFigures, hcDisabled, saveFigureAtWardrobeIndex, wearFigureAtIndex ]);
return (
<AutoGrid columnCount={ 5 } columnMinWidth={ 80 } columnMinHeight={ 140 }>
{ figures }
</AutoGrid>
);
}

View File

@ -1,35 +0,0 @@
import { FC, useEffect, useState } from 'react';
import { AvatarEditorGridPartItem, GetConfigurationValue } from '../../../../api';
import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../../common';
import { AvatarEditorIcon } from '../AvatarEditorIcon';
export interface AvatarEditorFigureSetItemViewProps extends LayoutGridItemProps
{
partItem: AvatarEditorGridPartItem;
}
export const AvatarEditorFigureSetItemView: FC<AvatarEditorFigureSetItemViewProps> = props =>
{
const { partItem = null, children = null, ...rest } = props;
const [ updateId, setUpdateId ] = useState(-1);
const hcDisabled = GetConfigurationValue<boolean>('hc.disabled', false);
useEffect(() =>
{
const rerender = () => setUpdateId(prevValue => (prevValue + 1));
partItem.notify = rerender;
return () => partItem.notify = null;
}, [ partItem ]);
return (
<LayoutGridItem itemImage={ (partItem.isClear ? undefined : partItem.imageUrl) } itemActive={ partItem.isSelected } { ...rest }>
{ !hcDisabled && partItem.isHC && <LayoutCurrencyIcon className="position-absolute end-1 bottom-1" type="hc" /> }
{ partItem.isClear && <AvatarEditorIcon icon="clear" /> }
{ partItem.isSellable && <AvatarEditorIcon icon="sellable" position="absolute" className="end-1 bottom-1" /> }
{ children }
</LayoutGridItem>
);
}

View File

@ -1,44 +0,0 @@
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from 'react';
import { AvatarEditorGridPartItem, CategoryData, IAvatarEditorCategoryModel } from '../../../../api';
import { AutoGrid } from '../../../../common';
import { AvatarEditorFigureSetItemView } from './AvatarEditorFigureSetItemView';
export interface AvatarEditorFigureSetViewProps
{
model: IAvatarEditorCategoryModel;
category: CategoryData;
setMaxPaletteCount: Dispatch<SetStateAction<number>>;
}
export const AvatarEditorFigureSetView: FC<AvatarEditorFigureSetViewProps> = props =>
{
const { model = null, category = null, setMaxPaletteCount = null } = props;
const elementRef = useRef<HTMLDivElement>(null);
const selectPart = useCallback((item: AvatarEditorGridPartItem) =>
{
const index = category.parts.indexOf(item);
if(index === -1) return;
model.selectPart(category.name, index);
const partItem = category.getCurrentPart();
setMaxPaletteCount(partItem.maxColorIndex || 1);
}, [ model, category, setMaxPaletteCount ]);
useEffect(() =>
{
if(!model || !category || !elementRef || !elementRef.current) return;
elementRef.current.scrollTop = 0;
}, [ model, category ]);
return (
<AutoGrid innerRef={ elementRef } columnCount={ 3 } columnMinHeight={ 50 }>
{ (category.parts.length > 0) && category.parts.map((item, index) =>
<AvatarEditorFigureSetItemView key={ index } partItem={ item } onClick={ event => selectPart(item) } />) }
</AutoGrid>
);
}

View File

@ -1,2 +0,0 @@
export * from './AvatarEditorFigureSetItemView';
export * from './AvatarEditorFigureSetView';

View File

@ -1,6 +0,0 @@
export * from './AvatarEditorFigurePreviewView';
export * from './AvatarEditorIcon';
export * from './AvatarEditorModelView';
export * from './AvatarEditorWardrobeView';
export * from './figure-set';
export * from './palette-set';

View File

@ -1,32 +0,0 @@
import { FC, useEffect, useState } from 'react';
import { AvatarEditorGridColorItem, GetConfigurationValue } from '../../../../api';
import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../../common';
export interface AvatarEditorPaletteSetItemProps extends LayoutGridItemProps
{
colorItem: AvatarEditorGridColorItem;
}
export const AvatarEditorPaletteSetItem: FC<AvatarEditorPaletteSetItemProps> = props =>
{
const { colorItem = null, children = null, ...rest } = props;
const [ updateId, setUpdateId ] = useState(-1);
const hcDisabled = GetConfigurationValue<boolean>('hc.disabled', false);
useEffect(() =>
{
const rerender = () => setUpdateId(prevValue => (prevValue + 1));
colorItem.notify = rerender;
return () => colorItem.notify = null;
}, [ colorItem ]);
return (
<LayoutGridItem itemHighlight itemColor={ colorItem.color } itemActive={ colorItem.isSelected } className="clear-bg" { ...rest }>
{ !hcDisabled && colorItem.isHC && <LayoutCurrencyIcon className="position-absolute end-1 bottom-1" type="hc" /> }
{ children }
</LayoutGridItem>
);
}

View File

@ -1,41 +0,0 @@
import { FC, useCallback, useEffect, useRef } from 'react';
import { AvatarEditorGridColorItem, CategoryData, IAvatarEditorCategoryModel } from '../../../../api';
import { AutoGrid } from '../../../../common';
import { AvatarEditorPaletteSetItem } from './AvatarEditorPaletteSetItemView';
export interface AvatarEditorPaletteSetViewProps
{
model: IAvatarEditorCategoryModel;
category: CategoryData;
paletteSet: AvatarEditorGridColorItem[];
paletteIndex: number;
}
export const AvatarEditorPaletteSetView: FC<AvatarEditorPaletteSetViewProps> = props =>
{
const { model = null, category = null, paletteSet = [], paletteIndex = -1 } = props;
const elementRef = useRef<HTMLDivElement>(null);
const selectColor = useCallback((item: AvatarEditorGridColorItem) =>
{
const index = paletteSet.indexOf(item);
if(index === -1) return;
model.selectColor(category.name, index, paletteIndex);
}, [ model, category, paletteSet, paletteIndex ]);
useEffect(() =>
{
if(!model || !category || !elementRef || !elementRef.current) return;
elementRef.current.scrollTop = 0;
}, [ model, category ]);
return (
<AutoGrid innerRef={ elementRef } gap={ 1 } columnCount={ 5 } columnMinWidth={ 30 }>
{ (paletteSet.length > 0) && paletteSet.map((item, index) =>
<AvatarEditorPaletteSetItem key={ index } colorItem={ item } onClick={ event => selectColor(item) } />) }
</AutoGrid>
);
}

View File

@ -1,2 +0,0 @@
export * from './AvatarEditorPaletteSetItemView';
export * from './AvatarEditorPaletteSetView';

View File

@ -124,9 +124,10 @@
position: relative; position: relative;
padding-left:38px; padding-left:38px;
text-align: left; text-align: left;
pointer-events: all;
&.friend-bar-item-active { &.friend-bar-item-active {
margin-bottom:21px;
} }
&.friend-bar-search-item-active { &.friend-bar-search-item-active {
@ -177,6 +178,23 @@
} }
} }
.friends-myinfo {
> :first-child {
border-bottom: 1px dashed white;
}
.myinfo-avatar {
height: 60px;
width: 60px;
display: flex;
justify-content: center;
.avatar-image {
margin-top: -17px;
}
}
}
.nitro-friends-messenger { .nitro-friends-messenger {
width: $messenger-width; width: $messenger-width;
height: $messenger-height; height: $messenger-height;

View File

@ -49,10 +49,10 @@ export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props =>
return ( return (
<div ref={ elementRef } className={ 'btn btn-friendsgensuccess friend-bar-item ' + (isVisible ? 'friend-bar-item-active' : '') } onClick={ event => setVisible(prevValue => !prevValue) }> <div ref={ elementRef } className={ 'btn btn-friendsgensuccess friend-bar-item ' + (isVisible ? 'friend-bar-item-active' : '') } onClick={ event => setVisible(prevValue => !prevValue) }>
<div className={ `friend-bar-item-head position-absolute ${ friend.id > 0 ? 'avatar': 'group' }` }> <div className={ `friend-bar-item-head position-absolute ${ friend.id > 0 ? 'avatar': 'group' }` }>
{ (friend.id > 0) && <LayoutAvatarImageView headOnly={ !isVisible } figure={ friend.figure } direction={ 2 } /> } { (friend.id > 0) && <LayoutAvatarImageView headOnly={ !isVisible } figure={ friend.figure } direction={ isVisible ? 2 : 3 } /> }
<LayoutAvatarImageView headOnly={true} figure={ <LayoutAvatarImageView headOnly={ !isVisible } figure={
friend.id > 0 ? friend.figure : (friend.id <= 0 && friend.figure === 'ADM') ? 'ha-3409-1413-70.lg-285-89.ch-3032-1334-109.sh-3016-110.hd-185-1359.ca-3225-110-62.wa-3264-62-62.fa-1206-90.hr-3322-1403' : friend.figure friend.id > 0 ? friend.figure : (friend.id <= 0 && friend.figure === 'ADM') ? 'ha-3409-1413-70.lg-285-89.ch-3032-1334-109.sh-3016-110.hd-185-1359.ca-3225-110-62.wa-3264-62-62.fa-1206-90.hr-3322-1403' : friend.figure
} isgroup={friend.id <= 0 ? 1 : 0} direction={friend.id > 0 ? 2 : 3} /> } isgroup={friend.id <= 0 ? 1 : 0} direction={isVisible ? 2 : 3} />
</div> </div>
<div className="text-truncate">{ friend.name }</div> <div className="text-truncate">{ friend.name }</div>

View File

@ -117,13 +117,12 @@ export const FriendsMessengerView: FC<{}> = props =>
{ visibleThreads && (visibleThreads.length > 0) && visibleThreads.map(thread => { { visibleThreads && (visibleThreads.length > 0) && visibleThreads.map(thread => {
return ( return (
<LayoutGridItem key={ thread.threadId } itemActive={ (activeThread === thread) } onClick={ event => setActiveThreadId(thread.threadId) }> <LayoutGridItem key={ thread.threadId } itemActive={ (activeThread === thread) } onClick={ event => setActiveThreadId(thread.threadId) }>
{ thread.unread && { thread.unread && <LayoutItemCountView count={ thread.unreadCount } /> }
<LayoutItemCountView count={ thread.unreadCount } /> }
<Flex fullWidth alignItems="center" gap={ 1 }> <Flex fullWidth alignItems="center" gap={ 1 }>
<Flex alignItems="center" className="friend-head px-1"> <Flex alignItems="center" className="friend-head px-2">
<LayoutAvatarImageView figure={ <LayoutAvatarImageView figure={
thread.participant.id > 0 ? thread.participant.figure : thread.participant.id <= 0 && thread.participant.figure === 'ADM' ? 'ha-3409-1413-70.lg-285-89.ch-3032-1334-109.sh-3016-110.hd-185-1359.ca-3225-110-62.wa-3264-62-62.fa-1206-90.hr-3322-1403' : thread.participant.figure thread.participant.id > 0 ? thread.participant.figure : thread.participant.id <= 0 && thread.participant.figure === 'ADM' ? 'ha-3409-1413-70.lg-285-89.ch-3032-1334-109.sh-3016-110.hd-185-1359.ca-3225-110-62.wa-3264-62-62.fa-1206-90.hr-3322-1403' : thread.participant.figure
} headOnly={true} direction={thread.participant.id > 0 ? 2 : 3} scale={0.9} /> } headOnly={true} direction={thread.participant.id > 0 ? 2 : 3} />
</Flex> </Flex>
<Text truncate grow>{ thread.participant.name }</Text> <Text truncate grow>{ thread.participant.name }</Text>
</Flex> </Flex>
@ -138,19 +137,17 @@ export const FriendsMessengerView: FC<{}> = props =>
<> <>
<Text bold center>{ LocalizeText('messenger.window.separator', [ 'FRIEND_NAME' ], [ activeThread.participant.name ]) }</Text> <Text bold center>{ LocalizeText('messenger.window.separator', [ 'FRIEND_NAME' ], [ activeThread.participant.name ]) }</Text>
<Flex alignItems="center" justifyContent="between" gap={ 1 }> <Flex alignItems="center" justifyContent="between" gap={ 1 }>
<Flex gap={ 1 }> {activeThread && activeThread.participant.id > 0 && (
<Flex gap={1}>
<ButtonGroup> <ButtonGroup>
<Button onClick={ followFriend }> <Button onClick={followFriend}><Base className="nitro-friends-spritesheet icon-follow" /></Button>
<Base className="nitro-friends-spritesheet icon-follow" /> <Button onClick={openProfile}><Base className="nitro-friends-spritesheet icon-profile-sm" /></Button>
</Button>
<Button onClick={ openProfile }>
<Base className="nitro-friends-spritesheet icon-profile-sm" />
</Button>
</ButtonGroup> </ButtonGroup>
<Button variant="danger" onClick={ () => report(ReportType.IM, { reportedUserId: activeThread.participant.id }) }> <Button variant="danger" onClick={() => report(ReportType.IM, { reportedUserId: activeThread.participant.id })}>
{ LocalizeText('messenger.window.button.report') } {LocalizeText('messenger.window.button.report')}
</Button> </Button>
</Flex> </Flex>
)}
<Button onClick={ event => closeThread(activeThread.threadId) }> <Button onClick={ event => closeThread(activeThread.threadId) }>
<FaTimes className="fa-icon" /> <FaTimes className="fa-icon" />
</Button> </Button>

View File

@ -5,6 +5,7 @@ import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, Nitro
import { useInventoryTrade, useInventoryUnseenTracker, useMessageEvent, useNitroEvent } from '../../hooks'; import { useInventoryTrade, useInventoryUnseenTracker, useMessageEvent, useNitroEvent } from '../../hooks';
import { InventoryBadgeView } from './views/badge/InventoryBadgeView'; import { InventoryBadgeView } from './views/badge/InventoryBadgeView';
import { InventoryBotView } from './views/bot/InventoryBotView'; import { InventoryBotView } from './views/bot/InventoryBotView';
import { InventoryFurnitureDeleteView } from './views/furniture/InventoryFurnitureDeleteView';
import { InventoryFurnitureView } from './views/furniture/InventoryFurnitureView'; import { InventoryFurnitureView } from './views/furniture/InventoryFurnitureView';
import { InventoryTradeView } from './views/furniture/InventoryTradeView'; import { InventoryTradeView } from './views/furniture/InventoryTradeView';
import { InventoryPetView } from './views/pet/InventoryPetView'; import { InventoryPetView } from './views/pet/InventoryPetView';
@ -142,6 +143,7 @@ export const InventoryView: FC<{}> = props =>
{ (currentTab === TAB_BADGES ) && { (currentTab === TAB_BADGES ) &&
<InventoryBadgeView /> } <InventoryBadgeView /> }
</NitroCardContentView> </NitroCardContentView>
<InventoryFurnitureDeleteView />
</> } </> }
{ isTrading && { isTrading &&
<NitroCardContentView> <NitroCardContentView>

View File

@ -0,0 +1,96 @@
import { DeleteItemMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { FaCaretLeft, FaCaretRight } from 'react-icons/fa';
import { FurnitureItem, LocalizeText, ProductTypeEnum, SendMessageComposer } from '../../../../api';
import { Button, Column, Flex, Grid, LayoutFurniImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { DeleteItemConfirmEvent } from '../../../../events';
import { useNotification, useUiEvent } from '../../../../hooks';
export const InventoryFurnitureDeleteView : FC<{}> = props =>
{
const [ item, setItem ] = useState<FurnitureItem>(null);
const [ amount, setAmount ] = useState(1);
const [ tempAmount, setTempAmount ] = useState('1');
const [ maxAmount, setMaxAmount ] = useState(1);
const updateAmount = (amnt: string) =>
{
let newValue: number = parseInt(amnt);
if(isNaN(newValue) || (newValue === amount)) return;
newValue = Math.max(newValue, 1);
newValue = Math.min(newValue, maxAmount);
if(newValue === amount) return;
setTempAmount(newValue.toString());
setAmount(newValue);
}
const { showConfirm = null } = useNotification();
useUiEvent<DeleteItemConfirmEvent>(DeleteItemConfirmEvent.DELETE_ITEM_CONFIRM, event => {
setItem(event.item);
setMaxAmount(event.amount);
});
if(!item) return null;
const getFurniTitle = (item ? LocalizeText(item.isWallItem ? 'wallItem.name.' + item.type : 'roomItem.name.' + item.type) : '');
const getFurniDescription = (item ? LocalizeText(item.isWallItem ? 'wallItem.desc.' + item.type : 'roomItem.desc.' + item.type) : '');
const deleteItem = () =>
{
if(!item) return;
showConfirm(LocalizeText('inventory.delete.confirm_delete.info', [ 'furniname', 'amount' ], [ getFurniTitle, amount.toString() ]), () =>
{
SendMessageComposer(new DeleteItemMessageComposer(item.id, amount));
setItem(null);
setAmount(1);
setMaxAmount(1);
setTempAmount('1');
},
() =>
{
setItem(null);
setAmount(1);
setMaxAmount(1);
setTempAmount('1');
}, null, null, LocalizeText('inventory.delete.confirm_delete.title'));
}
return (
<NitroCardView className="nitro-catalog-layout-marketplace-post-offer" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('inventory.delete.confirm_delete.title') } onCloseClick={ event => { setItem(null); setAmount(1); setMaxAmount(1); setTempAmount('1'); } } />
<NitroCardContentView overflow="hidden">
<Grid fullHeight>
<Column center className="bg-muted rounded p-2" size={ 4 } overflow="hidden">
<LayoutFurniImageView productType={ item.isWallItem ? ProductTypeEnum.WALL : ProductTypeEnum.FLOOR } productClassId={ item.type } extraData={ item.extra.toString() } />
</Column>
<Column size={ 8 } justifyContent="between" overflow="hidden">
<Column grow gap={ 1 }>
<Text fontWeight="bold">{ getFurniTitle }</Text>
<Text truncate shrink>{ getFurniDescription }</Text>
</Column>
<Column overflow="auto">
<Text>{ LocalizeText('inventory.delete.amount') }</Text>
<Flex alignItems="center" gap={ 1 }>
<FaCaretLeft className="text-black cursor-pointer fa-icon" onClick={ event => updateAmount((amount - 1).toString()) } />
<input className="form-control form-control-sm quantity-input" type="number" min={ 1 } max={ maxAmount } value={ tempAmount } onChange={ event => updateAmount(event.target.value) } placeholder={ LocalizeText('inventory.delete.amount') } />
<FaCaretRight className="text-black cursor-pointer fa-icon" onClick={ event => updateAmount((amount + 1).toString()) } />
<Button onClick={ event => updateAmount(maxAmount.toString()) }>
{ LocalizeText('inventory.delete.max_amount.button') }
</Button>
</Flex>
<Button disabled={ (amount > maxAmount) } onClick={ deleteItem }>
{ LocalizeText('inventory.delete.confirm_delete.button') }
</Button>
</Column>
</Column>
</Grid>
</NitroCardContentView>
</NitroCardView>
)
}

View File

@ -3,6 +3,8 @@ import { FC, useEffect, useState } from 'react';
import { DispatchUiEvent, FurniCategory, GroupItem, LocalizeText, UnseenItemCategory, attemptItemPlacement } from '../../../../api'; import { DispatchUiEvent, FurniCategory, GroupItem, LocalizeText, UnseenItemCategory, attemptItemPlacement } from '../../../../api';
import { AutoGrid, Button, Column, Grid, LayoutLimitedEditionCompactPlateView, LayoutRarityLevelView, LayoutRoomPreviewerView, Text } from '../../../../common'; import { AutoGrid, Button, Column, Grid, LayoutLimitedEditionCompactPlateView, LayoutRarityLevelView, LayoutRoomPreviewerView, Text } from '../../../../common';
import { CatalogPostMarketplaceOfferEvent } from '../../../../events'; import { CatalogPostMarketplaceOfferEvent } from '../../../../events';
import { DeleteItemConfirmEvent } from '../../../../events';
import { FaTrashAlt } from 'react-icons/fa'
import { useInventoryFurni, useInventoryUnseenTracker } from '../../../../hooks'; import { useInventoryFurni, useInventoryUnseenTracker } from '../../../../hooks';
import { InventoryCategoryEmptyView } from '../InventoryCategoryEmptyView'; import { InventoryCategoryEmptyView } from '../InventoryCategoryEmptyView';
import { InventoryFurnitureItemView } from './InventoryFurnitureItemView'; import { InventoryFurnitureItemView } from './InventoryFurnitureItemView';
@ -14,6 +16,11 @@ interface InventoryFurnitureViewProps
roomPreviewer: RoomPreviewer; roomPreviewer: RoomPreviewer;
} }
const FILTER_TYPE_EVERYTHING = 'inventory.filter.option.everything';
const FILTER_TYPE_FLOOR = 'inventory.furni.tab.floor';
const FILTER_TYPE_WALL = 'inventory.furni.tab.wall';
const FILTER_TYPE_WIRED = 'inventory.furni.tab.wired';
const attemptPlaceMarketplaceOffer = (groupItem: GroupItem) => const attemptPlaceMarketplaceOffer = (groupItem: GroupItem) =>
{ {
const item = groupItem.getLastItem(); const item = groupItem.getLastItem();
@ -25,6 +32,15 @@ const attemptPlaceMarketplaceOffer = (groupItem: GroupItem) =>
DispatchUiEvent(new CatalogPostMarketplaceOfferEvent(item)); DispatchUiEvent(new CatalogPostMarketplaceOfferEvent(item));
} }
const attemptDeleteItem = (groupItem: GroupItem) =>
{
const item = groupItem.getLastItem();
if(!item) return false;
DispatchUiEvent(new DeleteItemConfirmEvent(item, groupItem.getTotalCount()));
}
export const InventoryFurnitureView: FC<InventoryFurnitureViewProps> = props => export const InventoryFurnitureView: FC<InventoryFurnitureViewProps> = props =>
{ {
const { roomSession = null, roomPreviewer = null } = props; const { roomSession = null, roomPreviewer = null } = props;
@ -32,6 +48,32 @@ export const InventoryFurnitureView: FC<InventoryFurnitureViewProps> = props =>
const [ filteredGroupItems, setFilteredGroupItems ] = useState<GroupItem[]>([]); const [ filteredGroupItems, setFilteredGroupItems ] = useState<GroupItem[]>([]);
const { groupItems = [], selectedItem = null, activate = null, deactivate = null } = useInventoryFurni(); const { groupItems = [], selectedItem = null, activate = null, deactivate = null } = useInventoryFurni();
const { resetItems = null } = useInventoryUnseenTracker(); const { resetItems = null } = useInventoryUnseenTracker();
const [ filterType = string, setFilterType ] = useState(FILTER_TYPE_EVERYTHING);
useEffect(() =>
{
const filteredItems = groupItems.filter(item =>
{
const isWallItem = item.isWallItem;
const isFloorItem = !isWallItem;
const isWiredItem = item.name.startsWith('WIRED');
switch (filterType)
{
case FILTER_TYPE_WALL:
return isWallItem;
case FILTER_TYPE_FLOOR:
return isFloorItem;
case FILTER_TYPE_WIRED:
return isWiredItem;
case FILTER_TYPE_EVERYTHING:
default:
return true;
}
});
setFilteredGroupItems(filteredItems);
}, [ groupItems, filterType ]);
useEffect(() => useEffect(() =>
{ {
@ -114,6 +156,11 @@ export const InventoryFurnitureView: FC<InventoryFurnitureViewProps> = props =>
<Grid> <Grid>
<Column size={ 7 } overflow="hidden"> <Column size={ 7 } overflow="hidden">
<InventoryFurnitureSearchView groupItems={ groupItems } setGroupItems={ setFilteredGroupItems } /> <InventoryFurnitureSearchView groupItems={ groupItems } setGroupItems={ setFilteredGroupItems } />
<select className="form-select form-select-sm" value={ filterType } onChange={ e => setFilterType(e.target.value) }>
{ [ FILTER_TYPE_EVERYTHING, FILTER_TYPE_FLOOR, FILTER_TYPE_WALL, FILTER_TYPE_WIRED ].map(type => (
<option key={ type } value={ type }>{ LocalizeText(type) }</option>
)) }
</select>
<AutoGrid columnCount={ 5 }> <AutoGrid columnCount={ 5 }>
{ filteredGroupItems && (filteredGroupItems.length > 0) && filteredGroupItems.map((item, index) => <InventoryFurnitureItemView key={ index } groupItem={ item } />) } { filteredGroupItems && (filteredGroupItems.length > 0) && filteredGroupItems.map((item, index) => <InventoryFurnitureItemView key={ index } groupItem={ item } />) }
</AutoGrid> </AutoGrid>
@ -121,6 +168,11 @@ export const InventoryFurnitureView: FC<InventoryFurnitureViewProps> = props =>
<Column size={ 5 } overflow="auto"> <Column size={ 5 } overflow="auto">
<Column overflow="hidden" position="relative"> <Column overflow="hidden" position="relative">
<LayoutRoomPreviewerView roomPreviewer={ roomPreviewer } height={ 140 } /> <LayoutRoomPreviewerView roomPreviewer={ roomPreviewer } height={ 140 } />
{ selectedItem &&
<Button variant="danger" className="bottom-2 end-2" position="absolute" onClick={ event => attemptDeleteItem(selectedItem) }>
<FaTrashAlt className="fa-icon" />
</Button>
}
{ selectedItem && selectedItem.stuffData.isUnique && { selectedItem && selectedItem.stuffData.isUnique &&
<LayoutLimitedEditionCompactPlateView className="top-2 end-2" position="absolute" uniqueNumber={ selectedItem.stuffData.uniqueNumber } uniqueSeries={ selectedItem.stuffData.uniqueSeries } /> } <LayoutLimitedEditionCompactPlateView className="top-2 end-2" position="absolute" uniqueNumber={ selectedItem.stuffData.uniqueNumber } uniqueSeries={ selectedItem.stuffData.uniqueSeries } /> }
{ (selectedItem && selectedItem.stuffData.rarityLevel > -1) && { (selectedItem && selectedItem.stuffData.rarityLevel > -1) &&
@ -128,7 +180,7 @@ export const InventoryFurnitureView: FC<InventoryFurnitureViewProps> = props =>
</Column> </Column>
{ selectedItem && { selectedItem &&
<Column grow justifyContent="between" gap={ 2 }> <Column grow justifyContent="between" gap={ 2 }>
<Text grow truncate>{ selectedItem.name }</Text> <Text grow>{ selectedItem.name }</Text>
<Column gap={ 1 }> <Column gap={ 1 }>
{ !!roomSession && { !!roomSession &&
<Button variant="success" onClick={ event => attemptItemPlacement(selectedItem) }> <Button variant="success" onClick={ event => attemptItemPlacement(selectedItem) }>

View File

@ -1,26 +1,28 @@
.nitro-loading { .nitro-loading {
position: relative; position: relative;
background: #1d1a24; background-image: radial-gradient(#1d1a24, #003a6b);
z-index: 100; z-index: 100;
}
.connecting-duck { .connecting-duck {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
margin: auto; margin: auto;
right: 0; right: 0;
left: 0; left: 0;
background: url('@/assets/images/loading/caja.gif') no-repeat top left; background: url('@/assets/images/loading/loading.gif') no-repeat top left;
width: 84px; width: 84px;
height: 84px; height: 84px;
zoom: 1.5; zoom: 1.5;
image-rendering: pixelated; image-rendering: pixelated;
} }
.nitro-logo { .logo {
width: 47px; position: absolute;
height: 65px; top: 20px;
background: transparent url('https://assets.nitrodev.co/logos/react-loader.png') no-repeat center; left: 20px;
z-index: 1; background: url('@/assets/images/notifications/coolui.png') no-repeat top left; /* Fixed the typo here */
} width: 150px;
height: 100px;
} }

View File

@ -1,32 +1,30 @@
import { FC } from 'react'; import { FC } from 'react';
import { Base, Column, LayoutProgressBar, Text } from '../../common'; import { Base, Column, Text } from '../../common';
interface LoadingViewProps interface LoadingViewProps
{ {
isError: boolean; isError: boolean;
message: string; message: string;
percent: number;
} }
export const LoadingView: FC<LoadingViewProps> = props => export const LoadingView: FC<LoadingViewProps> = props =>
{ {
const { isError = false, message = '', percent = 0 } = props; const { isError = false, message = '' } = props;
return ( return (
<Column fullHeight position="relative" className="nitro-loading"> <Column fullHeight position="relative" className="nitro-loading">
<Base fullHeight className="container h-100"> <Base fullHeight className="container h-100">
<Column fullHeight alignItems="center" justifyContent="end"> <Column fullHeight alignItems="center" justifyContent="end">
<Base className="connecting-duck" /> <Base className="connecting-duck" />
<Base className="logo" />
<Column size={ 6 } className="text-center py-4"> <Column size={ 6 } className="text-center py-4">
{ isError && (message && message.length) ? { isError && (message && message.length) ?
<Base className="fs-4 text-shadow">{ message }</Base> <Base className="fs-4 text-shadow">{ message }</Base>
: :
<> <>
<Text fontSize={ 4 } variant="white" className="text-shadow">The hotel is loading { percent.toFixed() }%...</Text> <Text fontSize={ 4 } variant="white" className="text-shadow">The hotel is loading ...</Text>
<LayoutProgressBar progress={ percent } className="mt-2 large" />
</> </>
} }
</Column> </Column>
</Column> </Column>
</Base> </Base>

View File

@ -36,6 +36,13 @@
} }
} }
&.nitro-alert-credits {
width: 395px;
.notification-text {
min-width: auto;
}
}
&.nitro-alert-moderation, &.nitro-alert-moderation,
&.nitro-alert-alert { &.nitro-alert-alert {
width: 250px; width: 250px;
@ -57,3 +64,11 @@
height: 50px; height: 50px;
} }
} }
.nitro-coolui-logo {
width: 150px;
height: 78px;
position: relative;
background-image: url("@/assets/images/notifications/coolui.png");
background-repeat: no-repeat;
}

View File

@ -1,6 +1,6 @@
import { FC } from 'react'; import { FC } from 'react';
import { GetRendererVersion, GetUIVersion, NotificationAlertItem } from '../../../../api'; import { GetRendererVersion, GetUIVersion, NotificationAlertItem } from '../../../../api';
import { Button, Column, Flex, Grid, LayoutNotificationAlertView, LayoutNotificationAlertViewProps, Text } from '../../../../common'; import { Button, Column, Flex, Grid, LayoutNotificationCredits, LayoutNotificationAlertViewProps, Text } from '../../../../common';
interface NotificationDefaultAlertViewProps extends LayoutNotificationAlertViewProps interface NotificationDefaultAlertViewProps extends LayoutNotificationAlertViewProps
{ {
@ -9,15 +9,12 @@ interface NotificationDefaultAlertViewProps extends LayoutNotificationAlertViewP
export const NitroSystemAlertView: FC<NotificationDefaultAlertViewProps> = props => export const NitroSystemAlertView: FC<NotificationDefaultAlertViewProps> = props =>
{ {
const { title = 'Nitro', onClose = null, ...rest } = props; const { title = 'Nitro CoolUI Edit', onClose = null, ...rest } = props;
return ( return (
<LayoutNotificationAlertView title={ title } onClose={ onClose } { ...rest }> <LayoutNotificationCredits title={ title } onClose={ onClose } { ...rest }>
<Grid> <Grid>
<Column center size={ 5 }> <Column size={ 12 }>
<object data="https://assets.nitrodev.co/logos/nitro-n-dark.svg" width="100" height="100">&nbsp;</object>
</Column>
<Column size={ 7 }>
<Column alignItems="center" gap={ 0 }> <Column alignItems="center" gap={ 0 }>
<Text bold fontSize={ 4 }>Nitro React</Text> <Text bold fontSize={ 4 }>Nitro React</Text>
<Text>v{ GetUIVersion() }</Text> <Text>v{ GetUIVersion() }</Text>
@ -25,15 +22,29 @@ export const NitroSystemAlertView: FC<NotificationDefaultAlertViewProps> = props
<Column alignItems="center"> <Column alignItems="center">
<Text><b>Renderer:</b> v{ GetRendererVersion() }</Text> <Text><b>Renderer:</b> v{ GetRendererVersion() }</Text>
<Column fullWidth gap={ 1 }> <Column fullWidth gap={ 1 }>
<Button fullWidth variant="success" onClick={ event => window.open('https://discord.nitrodev.co') }>Discord</Button>
<Flex gap={ 1 }> <Flex gap={ 1 }>
<Button fullWidth onClick={ event => window.open('https://git.krews.org/nitro/nitro-react') }>Git</Button> <Button fullWidth variant="success" onClick={ event => window.open('https://discord.nitrodev.co') }>Nitro Discord</Button>
<Button fullWidth onClick={ event => window.open('https://git.krews.org/nitro/nitro-react/-/issues') }>Bug Report</Button> <Button fullWidth onClick={ event => window.open('https://git.krews.org/nitro/nitro-react') }>Nitro Git</Button>
</Flex> </Flex>
</Column> </Column>
</Column> </Column>
</Column>
<div className="nitro-coolui-logo"></div>
<Column size={ 10 }>
<Column alignItems="left" gap={ 0 }>
<Text center bold fontSize={ 5 }>Cool UI</Text>
<Text>Was created by Wassehk</Text>
<Text>- DuckieTM (Re-Design)</Text>
<Text>- Jonas (Contributing)</Text>
<Text>- Ohlucas (Sunset resources)</Text>
<Text center bold small>v2.2.0</Text>
<Flex gap={ 1 }>
<Button fullWidth variant="success" onClick={ event => window.open('https://discord.nitrodev.co') }>CoolUI Discord</Button>
<Button fullWidth onClick={ event => window.open('https://github.com/duckietm/Nitro-Cool-UI') }>CoolUI Git</Button>
</Flex>
</Column>
</Column> </Column>
</Grid> </Grid>
</LayoutNotificationAlertView> </LayoutNotificationCredits>
); );
} }

View File

@ -1,15 +1,16 @@
.nitro-room-tools-container { .nitro-room-tools-container {
position: absolute; position: absolute;
bottom: $toolbar-height + 65px; bottom: $toolbar-height + 25px;
left: 0; left: 0;
.nitro-room-tools { .nitro-room-tools {
background: rgba($dark, .95); background: #212131;
box-shadow: inset 0px 5px lighten(rgba($dark, .6), 2.5), inset 0 -4px darken(rgba($dark, .6), 4); box-shadow: inset 0px 5px lighten(rgba($dark, .6), 2.5), inset 0 -4px darken(rgba($dark, .6), 4);
border-top-right-radius: $border-radius; border-top-right-radius: $border-radius;
border-bottom-right-radius: $border-radius; border-bottom-right-radius: $border-radius;
transition: all .2s ease; transition: all .2s ease;
z-index: 2; z-index: 71;
margin-left: -20px;
.list-group-item { .list-group-item {
background: transparent; background: transparent;
@ -45,14 +46,30 @@
} }
} }
.nitro-room-history {
background: #212131;
box-shadow: inset 0px 5px lighten(rgba($dark, .6), 2.5), inset 0 -4px darken(rgba($dark, .6), 4);
transition: all .2s ease;
width: 150px;
overflow: hidden;
z-index: 3;
}
.nitro-room-tools-info { .nitro-room-tools-info {
background: rgba($dark, .95); background: #212131;
box-shadow: inset 0px 5px lighten(rgba($dark, .6), 2.5), inset 0 -4px darken(rgba($dark, .6), 4); box-shadow: inset 0px 5px lighten(rgba($dark, .6), 2.5), inset 0 -4px darken(rgba($dark, .6), 4);
transition: all .2s ease; transition: all .2s ease;
max-width: 250px; max-width: 250px;
} }
} }
.nitro-room-tools-history {
position: absolute;
left: calc(100% - 2px);
margin-left: 2px;
height: 5%;
}
.wordquiz-question { .wordquiz-question {
position: absolute; position: absolute;
top: 10px; top: 10px;
@ -99,24 +116,49 @@
height: $nitro-doorbell-height; height: $nitro-doorbell-height;
} }
.nitro-room-tools-history { .toggle-roomtool {
background: #212131; min-height: 95px;
box-shadow: inset 0 5px rgba(38, 38, 57, 0.6), inset 0 -4px rgba(25, 25, 37, 0.6); width: 20px;
border-radius: 5px; margin-left: -5px;
transition: all 0.2s ease; padding-left: 10px;
margin-left: 3px; z-index: 72;
max-height: 104px;
overflow-y: auto;
z-index: 5;
padding-left: 6px;
} }
.nitro-room-history-rooms { .room-tool-item {
position: fixed; height: 20px;
transition: all 0.2s ease; cursor: pointer;
max-height: 40px; }
margin-left: 2px;
bottom: 75px; .margin-icons {
margin-top: -14px;
}
.margin-button-history {
margin-left: 4px;
margin-right: 4px;
}
.text-no-promote {
font-size: 15px;
}
.icon-style {
margin-top: 1px;
margin-left: 4px;
}
.arrow-right-style {
margin-right: 4px;
}
.tags {
background-color: #1B2834;
color: #1A75A6;
font-size: 10px;
&:hover {
color: #419AD2;
}
} }
@import './avatar-info/AvatarInfoWidgetView'; @import './avatar-info/AvatarInfoWidgetView';

View File

@ -686,6 +686,186 @@
} }
} }
&.bubble-39 {
border-image-source: url("@/assets/images/chat/chatbubbles/bubble_39.png");
border-image-slice: 16 6 7 32 fill;
border-image-width: 16px 6px 7px 32px;
border-image-outset: 5px 0px 0px 3px;
.pointer {
background: url("@/assets/images/chat/chatbubbles/bubble_39_pointer.png");
}
}
&.bubble-40 {
border-image-source: url("@/assets/images/chat/chatbubbles/bubble_40.png");
border-image-slice: 16 6 7 32 fill;
border-image-width: 16px 6px 7px 32px;
border-image-outset: 5px 0px 0px 3px;
.pointer {
background: url("@/assets/images/chat/chatbubbles/bubble_40_pointer.png");
}
}
&.bubble-41 {
border-image-source: url("@/assets/images/chat/chatbubbles/bubble_41.png");
border-image-slice: 16 7 6 33 fill;
border-image-width: 16px 6px 6px 32px;
border-image-outset: 7px 0px 0px 5px;
.pointer {
background: url("@/assets/images/chat/chatbubbles/bubble_41_pointer.png");
}
}
&.bubble-42 {
border-image-source: url("@/assets/images/chat/chatbubbles/bubble_42.png");
border-image-slice: 16 7 6 33 fill;
border-image-width: 16px 6px 6px 32px;
border-image-outset: 7px 0px 0px 5px;
.pointer {
background: url("@/assets/images/chat/chatbubbles/bubble_42_pointer.png");
}
}
&.bubble-43 {
border-image-source: url("@/assets/images/chat/chatbubbles/bubble_43.png");
border-image-slice: 16 7 6 33 fill;
border-image-width: 16px 6px 6px 32px;
border-image-outset: 7px 0px 0px 5px;
.pointer {
background: url("@/assets/images/chat/chatbubbles/bubble_43_pointer.png");
}
}
&.bubble-44 {
border-image-source: url("@/assets/images/chat/chatbubbles/bubble_44.png");
border-image-slice: 16 7 6 33 fill;
border-image-width: 16px 6px 6px 32px;
border-image-outset: 7px 0px 0px 5px;
.pointer {
background: url("@/assets/images/chat/chatbubbles/bubble_44_pointer.png");
}
}
&.bubble-45 {
border-image-source: url("@/assets/images/chat/chatbubbles/bubble_45.png");
border-image-slice: 16 7 6 33 fill;
border-image-width: 16px 6px 6px 32px;
border-image-outset: 7px 0px 0px 5px;
.pointer {
background: url("@/assets/images/chat/chatbubbles/bubble_45_pointer.png");
}
}
&.bubble-46 {
border-image-source: url("@/assets/images/chat/chatbubbles/bubble_46.png");
border-image-slice: 16 7 6 33 fill;
border-image-width: 16px 6px 6px 32px;
border-image-outset: 7px 0px 0px 5px;
.pointer {
background: url("@/assets/images/chat/chatbubbles/bubble_46_pointer.png");
}
}
&.bubble-47 {
border-image-source: url("@/assets/images/chat/chatbubbles/bubble_47.png");
border-image-slice: 16 7 6 33 fill;
border-image-width: 16px 6px 6px 32px;
border-image-outset: 7px 0px 0px 5px;
.pointer {
background: url("@/assets/images/chat/chatbubbles/bubble_47_pointer.png");
}
}
&.bubble-48 {
border-image-source: url("@/assets/images/chat/chatbubbles/bubble_48.png");
border-image-slice: 16 7 6 33 fill;
border-image-width: 16px 6px 6px 32px;
border-image-outset: 7px 0px 0px 5px;
.pointer {
background: url("@/assets/images/chat/chatbubbles/bubble_48_pointer.png");
}
}
&.bubble-49 {
border-image-source: url("@/assets/images/chat/chatbubbles/bubble_49.png");
border-image-slice: 16 7 6 33 fill;
border-image-width: 16px 6px 6px 32px;
border-image-outset: 7px 0px 0px 5px;
.pointer {
background: url("@/assets/images/chat/chatbubbles/bubble_49_pointer.png");
}
}
&.bubble-50 {
border-image-source: url("@/assets/images/chat/chatbubbles/bubble_50.png");
border-image-slice: 16 7 6 33 fill;
border-image-width: 16px 6px 6px 32px;
border-image-outset: 7px 0px 0px 5px;
.pointer {
background: url("@/assets/images/chat/chatbubbles/bubble_50_pointer.png");
}
}
&.bubble-51 {
border-image-source: url("@/assets/images/chat/chatbubbles/bubble_51.png");
border-image-slice: 16 7 6 33 fill;
border-image-width: 16px 6px 6px 32px;
border-image-outset: 7px 0px 0px 5px;
.pointer {
background: url("@/assets/images/chat/chatbubbles/bubble_51_pointer.png");
}
}
&.bubble-52 {
border-image-source: url("@/assets/images/chat/chatbubbles/bubble_52.png");
border-image-slice: 16 7 6 33 fill;
border-image-width: 16px 6px 6px 32px;
border-image-outset: 7px 0px 0px 5px;
.pointer {
background: url("@/assets/images/chat/chatbubbles/bubble_52_pointer.png");
}
}
&.bubble-53 {
border-image-source: url("@/assets/images/chat/chatbubbles/bubble_53.png");
border-image-slice: 16 7 6 33 fill;
border-image-width: 16px 6px 6px 32px;
border-image-outset: 7px 0px 0px 5px;
.pointer {
background: url("@/assets/images/chat/chatbubbles/bubble_53_pointer.png");
}
}
.user-container { .user-container {
z-index: 3; z-index: 3;
display: flex; display: flex;
@ -916,4 +1096,64 @@
background: url('@/assets/images/chat/chatbubbles/bubble_38_extra.png'); background: url('@/assets/images/chat/chatbubbles/bubble_38_extra.png');
} }
} }
&.bubble-39 {
background-image: url("@/assets/images/chat/chatbubbles/bubble_39.png");
}
&.bubble-40 {
background-image: url("@/assets/images/chat/chatbubbles/bubble_40.png");
}
&.bubble-41 {
background-image: url("@/assets/images/chat/chatbubbles/bubble_41.png");
}
&.bubble-42 {
background-image: url("@/assets/images/chat/chatbubbles/bubble_42.png");
}
&.bubble-43 {
background-image: url("@/assets/images/chat/chatbubbles/bubble_43.png");
}
&.bubble-44 {
background-image: url("@/assets/images/chat/chatbubbles/bubble_44.png");
}
&.bubble-45 {
background-image: url("@/assets/images/chat/chatbubbles/bubble_45.png");
}
&.bubble-46 {
background-image: url("@/assets/images/chat/chatbubbles/bubble_46.png");
}
&.bubble-47 {
background-image: url("@/assets/images/chat/chatbubbles/bubble_47.png");
}
&.bubble-48 {
background-image: url("@/assets/images/chat/chatbubbles/bubble_48.png");
}
&.bubble-49 {
background-image: url("@/assets/images/chat/chatbubbles/bubble_49.png");
}
&.bubble-50 {
background-image: url("@/assets/images/chat/chatbubbles/bubble_50.png");
}
&.bubble-51 {
background-image: url("@/assets/images/chat/chatbubbles/bubble_51.png");
}
&.bubble-52 {
background-image: url("@/assets/images/chat/chatbubbles/bubble_52.png");
}
&.bubble-53 {
background-image: url("@/assets/images/chat/chatbubbles/bubble_53.png");
}
} }

View File

@ -1,21 +1,24 @@
import { CreateLinkEvent, GetGuestRoomResultEvent, GetRoomEngine, NavigatorSearchComposer, RateFlatMessageComposer } from '@nitrots/nitro-renderer'; import { CreateLinkEvent, GetGuestRoomResultEvent, GetRoomEngine, NavigatorSearchComposer, RateFlatMessageComposer, RoomDataParser } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { LocalizeText, SendMessageComposer, TryVisitRoom } from '../../../../api'; import { LocalizeText, SendMessageComposer, SetLocalStorage, TryVisitRoom } from '../../../../api';
import { Base, Column, Flex, Text, TransitionAnimation, TransitionAnimationTypes, classNames } from '../../../../common'; import { Base, Column, Flex, Text, TransitionAnimation, TransitionAnimationTypes, classNames } from '../../../../common';
import { useMessageEvent, useNavigator, useRoom } from '../../../../hooks'; import { useMessageEvent, useNavigator, useRoom } from '../../../../hooks';
export const RoomToolsWidgetView: FC<{}> = props => export const RoomToolsWidgetView: FC<{}> = props =>
{ {
const [ areBubblesMuted, setAreBubblesMuted ] = useState(false);
const [ isZoomedIn, setIsZoomedIn ] = useState<boolean>(false); const [ isZoomedIn, setIsZoomedIn ] = useState<boolean>(false);
const [ roomName, setRoomName ] = useState<string>(null); const [ roomName, setRoomName ] = useState<string>(null);
const [ roomOwner, setRoomOwner ] = useState<string>(null); const [ roomOwner, setRoomOwner ] = useState<string>(null);
const [ roomTags, setRoomTags ] = useState<string[]>(null); const [ roomTags, setRoomTags ] = useState<string[]>(null);
const [ isOpen, setIsOpen ] = useState<boolean>(false); const [ isOpen, setIsOpen ] = useState<boolean>(false);
const [ isOpenHistory, setIsOpenHistory ] = useState<boolean>(false); const [ isOpenHistory, setIsOpenHistory ] = useState<boolean>(false);
const { navigatorData = null } = useNavigator(); const [ show, setShow ] = useState(true);
const [ roomHistory, setRoomHistory ] = useState<{ roomId: number, roomName: string }[]>([]); const [ roomHistory, setRoomHistory ] = useState<{ roomId: number, roomName: string }[]>([]);
const { navigatorData = null } = useNavigator();
const { roomSession = null } = useRoom(); const { roomSession = null } = useRoom();
const [areBubblesMuted, setAreBubblesMuted] = useState(false);
useEffect(() => { if (!roomName) { setRoomName(LocalizeText('landing.view.generic.welcome.first_login')); } }, [roomName]);
const handleToolClick = (action: string, value?: string) => const handleToolClick = (action: string, value?: string) =>
{ {
@ -46,6 +49,11 @@ export const RoomToolsWidgetView: FC<{}> = props =>
if (bubbleElement) { if (bubbleElement) {
bubbleElement.classList.toggle('icon-chat-disablebubble'); bubbleElement.classList.toggle('icon-chat-disablebubble');
} }
const hiddenbubblesTextElement = document.getElementById('hiddenbubblesText');
if (hiddenbubblesTextElement) {
const newText = areBubblesMuted ? LocalizeText('room.unmute.button.text') : LocalizeText('room.mute.button.text');
hiddenbubblesTextElement.innerText = newText;
}
setAreBubblesMuted(!areBubblesMuted); setAreBubblesMuted(!areBubblesMuted);
const bubbleIcon = document.getElementById('bubbleIcon'); const bubbleIcon = document.getElementById('bubbleIcon');
if (bubbleIcon) { if (bubbleIcon) {
@ -63,53 +71,59 @@ export const RoomToolsWidgetView: FC<{}> = props =>
SendMessageComposer(new NavigatorSearchComposer('hotel_view', `tag:${ value }`)); SendMessageComposer(new NavigatorSearchComposer('hotel_view', `tag:${ value }`));
return; return;
case 'room_history': case 'room_history':
if (roomHistory.length > 0) { if (roomHistory.length > 0) setIsOpenHistory(prevValue => !prevValue);
const roomHistoryTool = document.getElementById("roomhistorytool");
if (!isOpenHistory) {
roomHistoryTool.style.display = "block";
setIsOpenHistory(true);
} else {
setIsOpenHistory(false);
roomHistoryTool.style.display = "none";
}
}
return; return;
case 'room_history_back': case 'room_history_back':
TryVisitRoom(roomHistory[roomHistory.findIndex(room => room.roomId === navigatorData.currentRoomId) - 1].roomId) TryVisitRoom(roomHistory[roomHistory.findIndex(room => room.roomId === navigatorData.currentRoomId) - 1].roomId);
return; return;
case 'room_history_next': case 'room_history_next':
TryVisitRoom(roomHistory[roomHistory.findIndex(room => room.roomId === navigatorData.currentRoomId) + 1].roomId) TryVisitRoom(roomHistory[roomHistory.findIndex(room => room.roomId === navigatorData.currentRoomId) + 1].roomId);
return; return;
} }
} }
const onChangeRoomHistory = (roomId: number, roomName: string) => { const onChangeRoomHistory = (roomId: number, roomName: string) =>
const newStorage = JSON.parse(window.localStorage.getItem('nitro.room.history')) || []; {
let newStorage = JSON.parse(window.localStorage.getItem('nitro.room.history'));
if (newStorage.some(room => room.roomId === roomId)) return; if (newStorage && newStorage.filter( (room: RoomDataParser) => room.roomId === roomId ).length > 0) return;
if (newStorage.length >= 10) newStorage.shift(); if (newStorage && newStorage.length >= 10) newStorage.shift();
const newData = [...newStorage, { roomId, roomName }]; const newData = !newStorage ? [ { roomId: roomId, roomName: roomName } ] : [ ...newStorage, { roomId: roomId, roomName: roomName } ];
setRoomHistory(newData); setRoomHistory(newData);
SetLocalStorage('nitro.room.history', newData); return SetLocalStorage('nitro.room.history', newData );
}; }
useMessageEvent<GetGuestRoomResultEvent>(GetGuestRoomResultEvent, event => { useMessageEvent<GetGuestRoomResultEvent>(GetGuestRoomResultEvent, event =>
CreateLinkEvent('nitrobubblehidden/hide'); {
const parser = event.getParser(); const parser = event.getParser();
if (!parser.roomEnter || (parser.data.roomId !== roomSession.roomId)) return; if(!parser.roomEnter || (parser.data.roomId !== roomSession.roomId)) return;
const { roomName, ownerName, tags } = parser.data; if(roomName !== parser.data.roomName) setRoomName(parser.data.roomName);
if(roomOwner !== parser.data.ownerName) setRoomOwner(parser.data.ownerName);
if(roomTags !== parser.data.tags) setRoomTags(parser.data.tags);
if (roomName !== roomSession.roomName) { setRoomName(roomName); }
if (ownerName !== roomSession.ownerName) { setRoomOwner(ownerName); }
if (JSON.stringify(tags) !== JSON.stringify(roomSession.tags)) { setRoomTags(tags); }
onChangeRoomHistory(parser.data.roomId, parser.data.roomName); onChangeRoomHistory(parser.data.roomId, parser.data.roomName);
}); });
useEffect(() =>
{
const handleTabClose = () =>
{
if (JSON.parse(window.localStorage.getItem('nitro.room.history'))) window.localStorage.removeItem('nitro.room.history');
};
window.addEventListener('beforeunload', handleTabClose);
return () =>
{
window.removeEventListener('beforeunload', handleTabClose);
};
}, []);
useEffect(() => useEffect(() =>
{ {
setIsOpen(true); setIsOpen(true);
@ -117,58 +131,86 @@ export const RoomToolsWidgetView: FC<{}> = props =>
const timeout = setTimeout(() => setIsOpen(false), 5000); const timeout = setTimeout(() => setIsOpen(false), 5000);
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
}, [ roomName, roomOwner, roomTags ]); }, [ roomName, roomOwner, roomTags, show ]);
useEffect(() => { useEffect(() =>
setRoomHistory(JSON.parse(window.localStorage.getItem('nitro.room.history')) || []); {
}, []); setRoomHistory(JSON.parse(window.localStorage.getItem('nitro.room.history')) ?? []);
}, [ ]);
return ( return (
<Flex className="nitro-room-tools-container" gap={ 2 }> <Flex className="nitro-room-tools-container" gap={ 2 }>
<Column center className="nitro-room-tools p-2"> <div className="btn-toggle toggle-roomtool d-flex align-items-center" onClick={ () => setShow(!show) }>
<div className={ 'toggle-icon ' + (!show ? 'right' : 'left') } />
</div>
{ show && (
<>
<Column gap={ 0 } center className="nitro-room-tools p-3 px-3">
<Flex>
<Column center className="margin-icons p-2 gap-2">
<Base pointer title={ LocalizeText('room.settings.button.text') } className="icon icon-cog" onClick={ () => handleToolClick('settings') } /> <Base pointer title={ LocalizeText('room.settings.button.text') } className="icon icon-cog" onClick={ () => handleToolClick('settings') } />
<Base pointer title={ LocalizeText('room.zoom.button.text') } onClick={ () => handleToolClick('zoom') } className={ classNames('icon', (!isZoomedIn && 'icon-zoom-less'), (isZoomedIn && 'icon-zoom-more')) } /> <Base pointer title={ LocalizeText('room.zoom.button.text') } onClick={ () => handleToolClick('zoom') } className={ classNames('icon', (!isZoomedIn && 'icon-zoom-less'), (isZoomedIn && 'icon-zoom-more')) } />
<Base pointer title={areBubblesMuted ? LocalizeText('room.unmute.button.text') : LocalizeText('room.mute.button.text')} className={areBubblesMuted ? "iconleftgen icon icon-chat-disablebubble" : "iconleftgen icon icon-chat-enablebubble"} onClick={ () => handleToolClick('hiddenbubbles') } />
<Base pointer title={ LocalizeText('room.chathistory.button.text') } onClick={ () => handleToolClick('chat_history') } className="icon icon-chat-history" /> <Base pointer title={ LocalizeText('room.chathistory.button.text') } onClick={ () => handleToolClick('chat_history') } className="icon icon-chat-history" />
{ navigatorData.canRate && { navigatorData.canRate &&
<Base pointer title={ LocalizeText('room.like.button.text') } onClick={ () => handleToolClick('like_room') } className="icon icon-like-room" /> } <Base pointer title={ LocalizeText('room.like.button.text') } onClick={ () => handleToolClick('like_room') } className="icon icon-like-room" /> }
<Base pointer onClick={ () => handleToolClick('toggle_room_link') } className="icon icon-room-link" />
<Base pointer onClick={ () => handleToolClick('hiddenbubbles') } className={`icon ${areBubblesMuted ? 'icon-chat-disablebubble' : 'icon-chat-enablebubble'}`} />
</Column> </Column>
<Column justifyContent="center"> <Column className="d-flex flex-column">
<TransitionAnimation type={ TransitionAnimationTypes.SLIDE_LEFT } inProp={ isOpen } timeout={ 300 }> <Flex className="w-100 room-tool-item">
<Column center> <Text variant="muted" underline small onClick={ () => handleToolClick('settings') }>{ LocalizeText('room.settings.button.text') }</Text>
<Column className="nitro-room-tools-info rounded py-2 px-3"> </Flex>
<Column gap={ 1 }> <Flex className="w-100 room-tool-item">
<Text wrap variant="white" fontSize={ 4 }>{ roomName }</Text> <Text variant="muted" underline small onClick={ () => handleToolClick('zoom') }>{ LocalizeText('room.zoom.button.text') }</Text>
<Text variant="muted" fontSize={ 5 }>{ roomOwner }</Text> </Flex>
</Column> <Flex className="w-100 room-tool-item">
{ roomTags && roomTags.length > 0 && <Text variant="muted" underline small onClick={ () => handleToolClick('chat_history') }>{ LocalizeText('room.chathistory.button.text') }</Text></Flex>
<Flex gap={ 2 }> { navigatorData.canRate &&
{ roomTags.map((tag, index) => <Text key={ index } small pointer variant="white" className="rounded bg-primary p-1" onClick={ () => handleToolClick('navigator_search_tag', tag) }>#{ tag }</Text>) } <Flex className="w-100 room-tool-item">
<Text variant="muted" underline small onClick={ () => handleToolClick('like_room') }>{ LocalizeText('room.like.button.text') }</Text>
</Flex> } </Flex> }
<Flex className="w-100 room-tool-item">
<Text variant="muted" underline small onClick={ () => handleToolClick('toggle_room_link') }>{ LocalizeText('navigator.embed.caption') }</Text>
</Flex>
<Flex className="w-100 room-tool-item">
<Text variant="muted" underline small onClick={() => handleToolClick('hiddenbubbles')}> {areBubblesMuted ? LocalizeText('room.unmute.button.text') : LocalizeText('room.mute.button.text')}</Text>
</Flex>
</Column> </Column>
</Column> </Flex>
</TransitionAnimation> <Flex justifyContent="center">
</Column>
<Flex className="nitro-room-history-rooms" justifyContent="bottom">
<Base pointer={ roomHistory.length > 1 && roomHistory[0]?.roomId !== navigatorData.currentRoomId } title={ LocalizeText('room.history.button.back.tooltip') } className={ `icon ${ (roomHistory?.length === 0 || roomHistory[0]?.roomId === navigatorData.currentRoomId) ? 'icon-room-history-back-disabled' : 'icon-room-history-back-enabled' }` } onClick={ () => (roomHistory?.length === 0 || roomHistory[0]?.roomId === navigatorData.currentRoomId) ? null : handleToolClick('room_history_back') } /> <Base pointer={ roomHistory.length > 1 && roomHistory[0]?.roomId !== navigatorData.currentRoomId } title={ LocalizeText('room.history.button.back.tooltip') } className={ `icon ${ (roomHistory?.length === 0 || roomHistory[0]?.roomId === navigatorData.currentRoomId) ? 'icon-room-history-back-disabled' : 'icon-room-history-back-enabled' }` } onClick={ () => (roomHistory?.length === 0 || roomHistory[0]?.roomId === navigatorData.currentRoomId) ? null : handleToolClick('room_history_back') } />
<Base pointer={ roomHistory?.length > 0 } title={ LocalizeText('room.history.button.tooltip') } className={ `icon ${ roomHistory?.length === 0 ? 'icon-room-history-disabled' : 'icon-room-history-enabled' } margin-button-history` } onClick={ () => roomHistory?.length === 0 ? null : handleToolClick('room_history') } /> <Base pointer={ roomHistory?.length > 0 } title={ LocalizeText('room.history.button.tooltip') } className={ `icon ${ roomHistory?.length === 0 ? 'icon-room-history-disabled' : 'icon-room-history-enabled' } margin-button-history` } onClick={ () => roomHistory?.length === 0 ? null : handleToolClick('room_history') } />
<Base pointer={ roomHistory.length > 1 && roomHistory[roomHistory.length - 1]?.roomId !== navigatorData.currentRoomId } title={ LocalizeText('room.history.button.forward.tooltip') } className={ `icon ${ (roomHistory?.length === 0 || roomHistory[roomHistory.length - 1]?.roomId === navigatorData.currentRoomId) ? 'icon-room-history-next-disabled' : 'icon-room-history-next-enabled' }` } onClick={ () => (roomHistory?.length === 0 || roomHistory[roomHistory.length - 1]?.roomId === navigatorData.currentRoomId) ? null : handleToolClick('room_history_next') } /> <Base pointer={ roomHistory.length > 1 && roomHistory[roomHistory.length - 1]?.roomId !== navigatorData.currentRoomId } title={ LocalizeText('room.history.button.forward.tooltip') } className={ `icon ${ (roomHistory?.length === 0 || roomHistory[roomHistory.length - 1]?.roomId === navigatorData.currentRoomId) ? 'icon-room-history-next-disabled' : 'icon-room-history-next-enabled' }` } onClick={ () => (roomHistory?.length === 0 || roomHistory[roomHistory.length - 1]?.roomId === navigatorData.currentRoomId) ? null : handleToolClick('room_history_next') } />
</Flex> </Flex>
<div className="nitro-room-tools-history" id="roomhistorytool" style={ { display: "none",bottom: !navigatorData.canRate ? '180px' : '210px' } }> </Column>
<Flex className="nitro-room-tools-history" style={ { bottom: !navigatorData.canRate ? '180px' : '210px' } }>
<TransitionAnimation type={ TransitionAnimationTypes.SLIDE_LEFT } inProp={ isOpenHistory }> <TransitionAnimation type={ TransitionAnimationTypes.SLIDE_LEFT } inProp={ isOpenHistory }>
<Column center> <Column center>
<Column className="nitro-room-history rounded py-2 px-3"> <Column className="px-3 py-2 rounded nitro-room-history">
<Column gap={ 1 }> { <Column gap={ 1 }>
(roomHistory.length > 0) && roomHistory.map(history => { (roomHistory.length > 0) && roomHistory.map(history =>
{ {
return <Text key={ history.roomId } bold={ history.roomId === navigatorData.currentRoomId } variant={ history.roomId === navigatorData.currentRoomId ? 'white' : 'muted' } pointer onClick={ () => TryVisitRoom(history.roomId) }>{ history.roomName }</Text>; return <Text key={ history.roomId } bold={ history.roomId === navigatorData.currentRoomId } variant={ history.roomId === navigatorData.currentRoomId ? 'white' : 'muted' } pointer onClick={ () => TryVisitRoom(history.roomId) }>{ history.roomName }</Text>;
}) }) }
}
</Column> </Column>
</Column> </Column>
</Column> </Column>
</TransitionAnimation> </TransitionAnimation>
</div> </Flex>
<Column justifyContent="center">
<TransitionAnimation type={TransitionAnimationTypes.SLIDE_LEFT} inProp={isOpen} timeout={300}>
<Column center>
<Column className="px-3 py-2 rounded nitro-room-tools-info" overflow="hidden">
<Column gap={1}> <Text wrap variant="white" fontSize={4} truncate>{roomName}</Text> <Text variant="muted" fontSize={5} truncate>{roomOwner}</Text> </Column>
{roomTags && roomTags.length > 0 ? (
<Flex gap={2}> {roomTags.map((tag, index) => ( <Text key={index} small pointer truncate variant="white" className="rounded bg-primary p-1" onClick={() => handleToolClick('navigator_search_tag', tag)} > #{tag} </Text> ))}
</Flex> ) : ( <Text variant="muted"> { LocalizeText('navigator.notagsfound') } </Text> )}
</Column>
</Column>
</TransitionAnimation>
</Column>
</>
) }
</Flex> </Flex>
); );
} }

View File

@ -0,0 +1,27 @@
import { FurnitureItem } from '../../api';
import { NitroEvent } from '@nitrots/nitro-renderer';
export class DeleteItemConfirmEvent extends NitroEvent
{
public static DELETE_ITEM_CONFIRM = 'DIC_DELETE_ITEM_CONFIRM';
private _item: FurnitureItem;
private _amount: number;
constructor(item: FurnitureItem, amount: number)
{
super(DeleteItemConfirmEvent.DELETE_ITEM_CONFIRM);
this._item = item;
this._amount = amount;
}
public get item(): FurnitureItem
{
return this._item;
}
public get amount(): number
{
return this._amount;
}
}

View File

@ -1 +1,2 @@
export * from './DeleteItemConfirmEvent';
export * from './InventoryFurniAddedEvent'; export * from './InventoryFurniAddedEvent';

51
tailwind.config.js Normal file
View File

@ -0,0 +1,51 @@
/** @type {import('tailwindcss').Config} */
const { generateShades } = require('./css-utils/CSSColorUtils');
const colors = {
'toolbar': '#555555',
'card-header': '#1E7295',
'card-close': '#921911',
'card-tabs': '#185D79',
'card-border': '#283F5D',
'card-tab-item': '#B6BEC5',
'card-tab-item-active': '#DFDFDF',
'card-content-area': '#DFDFDF'
};
const boxShadow = {
'inner1px': 'inset 0 0 0 1px rgba(255,255,255,.3)'
};
module.exports = {
theme: {
extend: {
fontFamily: {
sans: [ 'Ubuntu', 'sans-serif' ],
},
colors: generateShades(colors),
boxShadow,
spacing: {
'card-header': '33px',
'card-tabs': '33px',
'navigator-w': '420px',
'navigator-h': '440px',
'inventory-w': '528px',
'inventory-h': '320px'
},
zIndex: {
'toolbar': ''
}
},
},
darkMode: 'class',
plugins: [
require('@tailwindcss/forms'),
require('@headlessui/tailwindcss')({ prefix: 'ui' })
],
content: [
'./index.html',
'./src/**/*.{html,js,jsx,ts,tsx}'
]
}

View File

@ -20,7 +20,10 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx",
"paths": {
"@layout/*": ["layout/*"],
}
}, },
"include": [ "include": [
"src", "src",

View File

@ -2,9 +2,10 @@
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { resolve } from 'path'; import { resolve } from 'path';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({ export default defineConfig({
plugins: [ react() ], plugins: [ react(), tsconfigPaths() ],
resolve: { resolve: {
alias: { alias: {
'@': resolve(__dirname, 'src'), '@': resolve(__dirname, 'src'),

3520
yarn.lock Normal file

File diff suppressed because it is too large Load Diff