Merge branch 'dev' into feature/guide-tool

This commit is contained in:
Bill 2022-02-22 21:54:44 -05:00
commit d359862e1c
910 changed files with 30336 additions and 32820 deletions

2
.env
View File

@ -1,2 +0,0 @@
BROWSER=none
GENERATE_SOURCEMAP=false

7
.env.example Normal file
View File

@ -0,0 +1,7 @@
BROWSER=none
GENERATE_SOURCEMAP=false
REACT_APP_CONFIG_URLS=/renderer-config.json,/ui-config.json
REACT_APP_SOCKET_URL=wss://ws.server.com:2096
REACT_APP_ASSET_URL=https://nitro.server.com
REACT_APP_IMAGE_LIBRARY_URL=https://swf.server.com/c_images/
REACT_APP_HOF_FURNI_URL=https://swf.server.com/dcr/hof_furni

34
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: Build
on:
push:
branches: [dev]
jobs:
build:
runs-on: dedicated-server
strategy:
matrix:
node-version: [16.x]
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"
- name: Install dependencies
run: |
yarn remove @nitrots/nitro-renderer
yarn add git+https://git@git.krews.org/nitro/nitro-renderer#dev
yarn install
- name: Build Nitro
run: |
yarn build
- name: Archive Artifacts
uses: actions/upload-artifact@v2
with:
path: |
build

View File

@ -1,49 +0,0 @@
name: Deploy Bundle@dev
on:
push:
branches: [ dev ]
jobs:
build:
runs-on: self-hosted
strategy:
matrix:
node-version: [14.x]
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Cache dependencies
uses: actions/cache@v2
with:
path: '~/.npm'
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: |
npm uninstall @nitrots/nitro-renderer
npm install git+https://git@git.krews.org/nitro/nitro-renderer#dev
- name: Build Nitro
run: |
npm run build
- name: Archive Artifacts
uses: actions/upload-artifact@v2
with:
path: |
build
- name: Deploy Artifacts
uses: easingthemes/ssh-deploy@main
env:
REMOTE_HOST: ${{ secrets.HOST }}
REMOTE_PORT: ${{ secrets.PORT }}
REMOTE_USER: ${{ secrets.USERNAME }}
SSH_PRIVATE_KEY: ${{ secrets.SSHKEY }}
SOURCE: "build/"
TARGET: ${{ secrets.TARGET }}

29
.gitignore vendored
View File

@ -1,20 +1,7 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist /dist
/tmp /tmp
/out-tsc /out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules /node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea /.idea
.project .project
.classpath .classpath
@ -22,29 +9,23 @@ speed-measure-plugin*.json
*.launch *.launch
.settings/ .settings/
*.sublime-workspace *.sublime-workspace
# IDE - VSCode
.vscode/* .vscode/*
!.vscode/settings.json !.vscode/settings.json
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
.history/* .history/*
# misc
/.sass-cache /.sass-cache
/connect.lock /connect.lock
/coverage /coverage
/libpeerconnection.log *.log
npm-debug.log
yarn-error.log
testem.log
/typings
.git .git
# System Files
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Nitro
/build /build
*.zip *.zip
.env
public/renderer-config.json
public/ui-config.json

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
*.scss

19384
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,16 +5,19 @@
"scripts": { "scripts": {
"start": "craco start", "start": "craco start",
"build": "craco --max_old_space_size=8048 build", "build": "craco --max_old_space_size=8048 build",
"build:prod": "npm uninstall @nitrots/nitro-renderer && npm i git+https://git@git.krews.org/nitro/nitro-renderer#dev && npm i && npm run build", "build:prod": "npx browserslist@latest --update-db && yarn install && yarn build",
"test": "craco test", "test": "craco test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"dependencies": { "dependencies": {
"@craco/craco": "^6.3.0", "@craco/craco": "^6.3.0",
"@nitrots/nitro-renderer": "file:../nitro-renderer", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.16",
"@nitrots/nitro-renderer": "^1.1.4",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"node-sass": "^5.0.0", "node-sass": "^6.0.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-bootstrap": "^2.0.0-alpha.2", "react-bootstrap": "^2.0.0-alpha.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
@ -23,20 +26,16 @@
"react-transition-group": "^4.4.2", "react-transition-group": "^4.4.2",
"react-virtualized": "^9.22.3", "react-virtualized": "^9.22.3",
"react-youtube": "^7.13.1", "react-youtube": "^7.13.1",
"typescript": "^4.3.5", "typescript": "^4.3.5"
"web-vitals": "^1.1.2"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"@types/jest": "^26.0.24",
"@types/node": "^12.20.19", "@types/node": "^12.20.19",
"@types/react": "^17.0.15", "@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"@types/react-slider": "^1.3.1", "@types/react-slider": "^1.3.1",
"@types/react-transition-group": "^4.4.2", "@types/react-transition-group": "^4.4.2",
"@types/react-virtualized": "^9.21.13", "@types/react-virtualized": "^9.21.13",
"@types/styled-components": "^5.1.15",
"@typescript-eslint/eslint-plugin": "^4.29.1" "@typescript-eslint/eslint-plugin": "^4.29.1"
} }
} }

34
post-install.js Normal file
View File

@ -0,0 +1,34 @@
import { request as httpsRequest } from 'https';
function install()
{
try
{
const params = {};
params['packageName'] = process.env.npm_package_name;
params['packageVersion'] = process.env.npm_package_version;
const data = JSON.stringify(params);
const request = httpsRequest({
hostname: 'install.nitrots.co',
port: 443,
path: '/collect',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': data.length
}
});
request.write(data);
request.end();
}
catch (e)
{
//
}
}
install();

View File

@ -13,9 +13,13 @@
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="w-100 h-100"></div> <div id="root" class="w-100 h-100"></div>
<script> <script>
var NitroConfig = { const NitroConfig = {
configurationUrls: [ '/renderer-config.json', '/ui-config.json' ], "config.urls": ("%REACT_APP_CONFIG_URLS%").split(','),
sso: (new URLSearchParams(window.location.search).get('sso') || null) "sso.ticket": (new URLSearchParams(window.location.search).get('sso') || null),
"socket.url": "%REACT_APP_SOCKET_URL%",
"asset.url": "%REACT_APP_ASSET_URL%",
"image.library.url": "%REACT_APP_IMAGE_LIBRARY_URL%",
"hof.furni.url": "%REACT_APP_HOF_FURNI_URL%"
}; };
</script> </script>
</body> </body>

View File

@ -1,8 +1,8 @@
{ {
"socket.url": "wss://ws.nitrots.co:2096", "socket.url": "",
"asset.url": "https://nitro.nitrots.co", "asset.url": "",
"image.library.url": "https://swf.nitrots.co/c_images/", "image.library.url": "",
"hof.furni.url": "https://swf.nitrots.co/dcr/hof_furni", "hof.furni.url": "",
"images.url": "${asset.url}/images", "images.url": "${asset.url}/images",
"gamedata.url": "${asset.url}/gamedata", "gamedata.url": "${asset.url}/gamedata",
"sounds.url": "${asset.url}/sounds/%sample%.mp3", "sounds.url": "${asset.url}/sounds/%sample%.mp3",
@ -21,9 +21,7 @@
"pet.asset.url": "${asset.url}/bundled/pet/%libname%.nitro", "pet.asset.url": "${asset.url}/bundled/pet/%libname%.nitro",
"generic.asset.url": "${asset.url}/bundled/generic/%libname%.nitro", "generic.asset.url": "${asset.url}/bundled/generic/%libname%.nitro",
"badge.asset.url": "${image.library.url}album1584/%badgename%.gif", "badge.asset.url": "${image.library.url}album1584/%badgename%.gif",
"badge.asset.group.url": "https://cdn.ironhotel.biz/group-badge/%badgedata%.gif", "badge.asset.grouparts.url": "${image.library.url}Badgeparts/badgepart_%part%.png",
"badge.asset.group.external.url": "",
"badge.asset.grouparts.url": "https://cdn.ironhotel.biz/static_iron_active/c_images/Badgeparts/badgepart_%part%.png",
"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": false,
@ -101,20 +99,8 @@
"elephants" "elephants"
], ],
"preload.assets.urls": [ "preload.assets.urls": [
"${images.url}/additions/user_blowkiss.png", "${asset.url}/bundled/generic/avatar_additions.nitro",
"${images.url}/additions/user_idle_left_1.png", "${asset.url}/bundled/generic/floor_editor.nitro",
"${images.url}/additions/user_idle_left_2.png",
"${images.url}/additions/user_idle_right_1.png",
"${images.url}/additions/user_idle_right_2.png",
"${images.url}/additions/user_muted.png",
"${images.url}/additions/user_muted_small.png",
"${images.url}/additions/user_typing.png",
"${images.url}/additions/number_1.png",
"${images.url}/additions/number_2.png",
"${images.url}/additions/number_3.png",
"${images.url}/additions/number_4.png",
"${images.url}/additions/number_5.png",
"${images.url}/additions/pet_experience_bubble.png",
"${images.url}/loading_icon.png", "${images.url}/loading_icon.png",
"${images.url}/clear_icon.png", "${images.url}/clear_icon.png",
"${images.url}/big_arrow.png" "${images.url}/big_arrow.png"

View File

@ -1,15 +1,16 @@
{ {
"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": "https://nitro.nitrots.co/camera", "camera.url": "https://camera.com",
"thumbnails.url": "https://nitro.nitrots.co/camera/thumbnail/%thumbnail%.png", "thumbnails.url": "https://camera.com/thumbnail/%thumbnail%.png",
"url.prefix": "http://localhost:3000", "url.prefix": "https://website.com",
"floorplan.tile.url": "${asset.url}/floorplan-editor/tiles.json", "floorplan.tile.url": "${asset.url}/floorplan-editor/tiles.json",
"habbopages.url": "${url.prefix}/", "habbopages.url": "${url.prefix}/",
"group.homepage.url": "${url.prefix}/groups/%groupid%/id", "group.homepage.url": "${url.prefix}/groups/%groupid%/id",
"guide.help.alpha.groupid": 0, "guide.help.alpha.groupid": 0,
"chat.viewer.height.percentage": 0.40, "chat.viewer.height.percentage": 0.40,
"widget.dimmer.colorwheel": false, "widget.dimmer.colorwheel": false,
"avatar.wardrobe.max.slots": 10,
"hotelview": { "hotelview": {
"widgets": { "widgets": {
"slot.1.widget": "promoarticle", "slot.1.widget": "promoarticle",
@ -46,9 +47,17 @@
"system.currency.types": [ "system.currency.types": [
-1, -1,
0, 0,
5, 5
101
], ],
"hc.center": {
"benefits.info": true,
"payday.info": true,
"gift.info": true,
"benefits.habbopage": "habboclub",
"payday.habbopage": "hcpayday",
"catalog.buy": "habbo_club",
"catalog.gifts": "club_gifts"
},
"currency.display.number.short": false, "currency.display.number.short": false,
"currency.asset.icon.url": "${images.url}/wallet/%type%.png", "currency.asset.icon.url": "${images.url}/wallet/%type%.png",
"catalog.asset.url": "${image.library.url}catalogue", "catalog.asset.url": "${image.library.url}catalogue",

View File

@ -17,8 +17,8 @@ $grid-active-border-color: $white;
$toolbar-height: 55px; $toolbar-height: 55px;
$achievement-width: 350px; $achievement-width: 375px;
$achievement-height: 370px; $achievement-height: 425px;
$avatar-editor-width: 620px; $avatar-editor-width: 620px;
$avatar-editor-height: 374px; $avatar-editor-height: 374px;
@ -50,16 +50,33 @@ $chat-history-height: 300px;
$friends-list-width: 250px; $friends-list-width: 250px;
$friends-list-height: 300px; $friends-list-height: 300px;
$help-width: 275px; $help-width: 450px;
$help-height: 450px; $help-height: 250px;
$nitropedia-width: 400px; $nitropedia-width: 400px;
$nitropedia-height: 400px; $nitropedia-height: 400px;
$messenger-width: 500px;
$messenger-height: 370px;
$marketplace-post-offer-width: 430px;
$marketplace-post-offer-height: 250px;
$camera-editor-width: 600px;
$camera-editor-height: 500px;
$camera-checkout-width: 350px;
$room-info-width: 325px;
$nitro-mod-tools-width: 175px;
.nitro-app { .nitro-app {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
@import './common';
@import "./layout/Layout"; @import "./layout/Layout";
@import './components';
@import "./views/Styles"; @import "./views/Styles";

View File

@ -1,13 +1,14 @@
import { ConfigurationEvent, LegacyExternalInterface, Nitro, NitroCommunicationDemoEvent, NitroEvent, NitroLocalizationEvent, NitroVersion, RoomEngineEvent, WebGL } from '@nitrots/nitro-renderer'; import { ConfigurationEvent, HabboWebTools, LegacyExternalInterface, Nitro, NitroCommunicationDemoEvent, NitroEvent, NitroLocalizationEvent, NitroVersion, RoomEngineEvent, WebGL } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react'; import { FC, useCallback, useState } from 'react';
import { GetCommunication, GetConfiguration, GetNitroInstance } from './api'; import { GetCommunication, GetConfiguration, GetNitroInstance } from './api';
import { Base } from './common';
import { LoadingView } from './components/loading/LoadingView';
import { MainView } from './components/main/MainView';
import { useConfigurationEvent } from './hooks/events/core/configuration/configuration-event'; import { useConfigurationEvent } from './hooks/events/core/configuration/configuration-event';
import { useLocalizationEvent } from './hooks/events/nitro/localization/localization-event'; import { useLocalizationEvent } from './hooks/events/nitro/localization/localization-event';
import { dispatchMainEvent, useMainEvent } from './hooks/events/nitro/main-event'; import { dispatchMainEvent, useMainEvent } from './hooks/events/nitro/main-event';
import { useRoomEngineEvent } from './hooks/events/nitro/room/room-engine-event'; import { useRoomEngineEvent } from './hooks/events/nitro/room/room-engine-event';
import { TransitionAnimation, TransitionAnimationTypes } from './layout'; import { TransitionAnimation, TransitionAnimationTypes } from './layout';
import { LoadingView } from './views/loading/LoadingView';
import { MainView } from './views/main/MainView';
export const App: FC<{}> = props => export const App: FC<{}> = props =>
{ {
@ -68,6 +69,8 @@ export const App: FC<{}> = props =>
setMessage('Finishing Up'); setMessage('Finishing Up');
GetNitroInstance().init(); GetNitroInstance().init();
if(LegacyExternalInterface.available) LegacyExternalInterface.call('legacyTrack', 'authentication', 'authok', []);
return; return;
case NitroCommunicationDemoEvent.CONNECTION_ERROR: case NitroCommunicationDemoEvent.CONNECTION_ERROR:
setIsError(true); setIsError(true);
@ -79,7 +82,7 @@ export const App: FC<{}> = props =>
setIsError(true); setIsError(true);
setMessage('Connection Error'); setMessage('Connection Error');
LegacyExternalInterface.call('disconnect', -1, 'client.init.handshake.fail'); HabboWebTools.send(-1, 'client.init.handshake.fail');
return; return;
case RoomEngineEvent.ENGINE_INITIALIZED: case RoomEngineEvent.ENGINE_INITIALIZED:
setIsReady(true); setIsReady(true);
@ -125,12 +128,13 @@ export const App: FC<{}> = props =>
} }
return ( return (
<div className="nitro-app overflow-hidden"> <Base fit overflow="hidden">
<div id="nitro-alerts-container" /> { (!isReady || isError) &&
{ (!isReady || isError) && <LoadingView isError={ isError } message={ message } /> } <LoadingView isError={ isError } message={ message } /> }
<TransitionAnimation type={ TransitionAnimationTypes.FADE_IN } inProp={ (isReady && !isError) }> <TransitionAnimation type={ TransitionAnimationTypes.FADE_IN } inProp={ (isReady && !isError) }>
<MainView /> <MainView />
</TransitionAnimation> </TransitionAnimation>
</div> <Base id="draggable-windows-container" />
</Base>
); );
} }

View File

@ -2,5 +2,7 @@ import { GetNitroInstance } from './GetNitroInstance';
export function CreateLinkEvent(link: string): void export function CreateLinkEvent(link: string): void
{ {
link = (link.startsWith('event:') ? link.substring(6) : link);
GetNitroInstance().createLinkEvent(link); GetNitroInstance().createLinkEvent(link);
} }

View File

@ -1,7 +1,7 @@
import { IFurnitureData, IGetImageListener, NitroEvent, NitroRenderTexture, PetFigureData, RoomObjectCategory, RoomObjectVariable, RoomSessionPresentEvent, RoomWidgetEnum, TextureUtils, Vector3d } from '@nitrots/nitro-renderer'; import { IFurnitureData, IGetImageListener, NitroEvent, NitroRenderTexture, PetFigureData, RoomObjectCategory, RoomObjectVariable, RoomSessionPresentEvent, RoomWidgetEnum, TextureUtils, Vector3d } from '@nitrots/nitro-renderer';
import { GetSessionDataManager, IsOwnerOfFurniture } from '../../..'; import { GetSessionDataManager, IsOwnerOfFurniture } from '../../..';
import { GetRoomEngine, LocalizeText } from '../../../..'; import { GetRoomEngine, LocalizeText } from '../../../..';
import { ProductTypeEnum } from '../../../../../views/catalog/common/ProductTypeEnum'; import { ProductTypeEnum } from '../../../../../components/catalog/common/ProductTypeEnum';
import { RoomWidgetUpdateEvent, RoomWidgetUpdatePresentDataEvent } from '../events'; import { RoomWidgetUpdateEvent, RoomWidgetUpdatePresentDataEvent } from '../events';
import { RoomWidgetFurniToWidgetMessage, RoomWidgetPresentOpenMessage } from '../messages'; import { RoomWidgetFurniToWidgetMessage, RoomWidgetPresentOpenMessage } from '../messages';
import { RoomWidgetMessage } from '../messages/RoomWidgetMessage'; import { RoomWidgetMessage } from '../messages/RoomWidgetMessage';

View File

@ -1,6 +1,6 @@
import { NitroEvent, RoomEngineUseProductEvent, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomSessionDanceEvent, RoomSessionUserDataUpdateEvent, RoomWidgetEnum } from '@nitrots/nitro-renderer'; import { NitroEvent, RoomEngineUseProductEvent, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomSessionDanceEvent, RoomSessionPetStatusUpdateEvent, RoomSessionUserDataUpdateEvent, RoomWidgetEnum } from '@nitrots/nitro-renderer';
import { GetRoomEngine, GetRoomSession, GetSessionDataManager, IsOwnerOfFurniture } from '../../../..'; import { GetRoomEngine, GetSessionDataManager, IsOwnerOfFurniture } from '../../../..';
import { FurniCategory } from '../../../../../views/inventory/common/FurniCategory'; import { FurniCategory } from '../../../../../components/inventory/common/FurniCategory';
import { RoomWidgetAvatarInfoEvent, RoomWidgetUpdateDanceStatusEvent, RoomWidgetUpdateEvent, RoomWidgetUpdateUserDataEvent, RoomWidgetUseProductBubbleEvent, UseProductItem } from '../events'; import { RoomWidgetAvatarInfoEvent, RoomWidgetUpdateDanceStatusEvent, RoomWidgetUpdateEvent, RoomWidgetUpdateUserDataEvent, RoomWidgetUseProductBubbleEvent, UseProductItem } from '../events';
import { RoomWidgetAvatarExpressionMessage, RoomWidgetChangePostureMessage, RoomWidgetDanceMessage, RoomWidgetMessage, RoomWidgetRoomObjectMessage, RoomWidgetUseProductMessage, RoomWidgetUserActionMessage } from '../messages'; import { RoomWidgetAvatarExpressionMessage, RoomWidgetChangePostureMessage, RoomWidgetDanceMessage, RoomWidgetMessage, RoomWidgetRoomObjectMessage, RoomWidgetUseProductMessage, RoomWidgetUserActionMessage } from '../messages';
import { RoomWidgetHandler } from './RoomWidgetHandler'; import { RoomWidgetHandler } from './RoomWidgetHandler';
@ -19,7 +19,7 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
let isDancing = false; let isDancing = false;
const userData = GetRoomSession().userDataManager.getUserData(GetSessionDataManager().userId); const userData = this.container.roomSession.userDataManager.getUserData(GetSessionDataManager().userId);
if(userData && (userData.roomIndex === danceEvent.roomIndex)) isDancing = (danceEvent.danceId !== 0); if(userData && (userData.roomIndex === danceEvent.roomIndex)) isDancing = (danceEvent.danceId !== 0);
@ -30,6 +30,9 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
case RoomEngineUseProductEvent.USE_PRODUCT_FROM_ROOM: case RoomEngineUseProductEvent.USE_PRODUCT_FROM_ROOM:
this.processUsableRoomObject((event as RoomEngineUseProductEvent).objectId); this.processUsableRoomObject((event as RoomEngineUseProductEvent).objectId);
return; return;
case RoomSessionPetStatusUpdateEvent.PET_STATUS_UPDATE:
this.processRoomSessionPetStatusUpdateEvent((event as RoomSessionPetStatusUpdateEvent));
return;
} }
} }
@ -44,28 +47,39 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
case RoomWidgetRoomObjectMessage.GET_OWN_CHARACTER_INFO: case RoomWidgetRoomObjectMessage.GET_OWN_CHARACTER_INFO:
this.processOwnCharacterInfo(); this.processOwnCharacterInfo();
break; break;
case RoomWidgetUserActionMessage.START_NAME_CHANGE:
// habbo help - start name change
break;
case RoomWidgetUserActionMessage.REQUEST_PET_UPDATE:
break;
case RoomWidgetUseProductMessage.PET_PRODUCT: {
const productMessage = (message as RoomWidgetUseProductMessage);
this.container.roomSession.usePetProduct(productMessage.objectId, productMessage.petId);
break;
}
case RoomWidgetUserActionMessage.HARVEST_PET:
this.container.roomSession.harvestPet(userId);
break;
case RoomWidgetUserActionMessage.COMPOST_PLANT:
this.container.roomSession.compostPlant(userId);
break;
case RoomWidgetDanceMessage.DANCE: { case RoomWidgetDanceMessage.DANCE: {
const danceMessage = (message as RoomWidgetDanceMessage); const danceMessage = (message as RoomWidgetDanceMessage);
GetRoomSession().sendDanceMessage(danceMessage.style); this.container.roomSession.sendDanceMessage(danceMessage.style);
break; break;
} }
case RoomWidgetAvatarExpressionMessage.AVATAR_EXPRESSION: { case RoomWidgetAvatarExpressionMessage.AVATAR_EXPRESSION: {
const expressionMessage = (message as RoomWidgetAvatarExpressionMessage); const expressionMessage = (message as RoomWidgetAvatarExpressionMessage);
GetRoomSession().sendExpressionMessage(expressionMessage.animation.ordinal) this.container.roomSession.sendExpressionMessage(expressionMessage.animation.ordinal)
break; break;
} }
case RoomWidgetChangePostureMessage.CHANGE_POSTURE: { case RoomWidgetChangePostureMessage.CHANGE_POSTURE: {
const postureMessage = (message as RoomWidgetChangePostureMessage); const postureMessage = (message as RoomWidgetChangePostureMessage);
GetRoomSession().sendPostureMessage(postureMessage.posture); this.container.roomSession.sendPostureMessage(postureMessage.posture);
break;
}
case RoomWidgetUseProductMessage.PET_PRODUCT: {
const productMessage = (message as RoomWidgetUseProductMessage);
GetRoomSession().usePetProduct(productMessage.objectId, productMessage.petId);
break; break;
} }
} }
@ -78,9 +92,11 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
const userId = GetSessionDataManager().userId; const userId = GetSessionDataManager().userId;
const userName = GetSessionDataManager().userName; const userName = GetSessionDataManager().userName;
const allowNameChange = GetSessionDataManager().canChangeName; const allowNameChange = GetSessionDataManager().canChangeName;
const userData = GetRoomSession().userDataManager.getUserData(userId); const userData = this.container.roomSession.userDataManager.getUserData(userId);
if(userData) this.container.eventDispatcher.dispatchEvent(new RoomWidgetAvatarInfoEvent(userId, userName, userData.type, userData.roomIndex, allowNameChange)); if(!userData) return;
this.container.eventDispatcher.dispatchEvent(new RoomWidgetAvatarInfoEvent(userId, userName, userData.type, userData.roomIndex, allowNameChange));
} }
private processUsableRoomObject(objectId: number): void private processUsableRoomObject(objectId: number): void
@ -120,24 +136,24 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
{ {
if(userData.ownerId === ownerId) if(userData.ownerId === ownerId)
{ {
if(userData.hasSaddle && (specialType === FurniCategory._Str_6096)) replace = true; if(userData.hasSaddle && (specialType === FurniCategory.PET_SADDLE)) replace = true;
const figureParts = userData.figure.split(' '); const figureParts = userData.figure.split(' ');
const figurePart = (figureParts.length ? parseInt(figureParts[0]) : -1); const figurePart = (figureParts.length ? parseInt(figureParts[0]) : -1);
if(figurePart === part) if(figurePart === part)
{ {
if(specialType === FurniCategory._Str_6915) if(specialType === FurniCategory.MONSTERPLANT_REVIVAL)
{ {
if(!userData.canRevive) continue; if(!userData.canRevive) continue;
} }
if(specialType === FurniCategory._Str_8726) if(specialType === FurniCategory.MONSTERPLANT_REBREED)
{ {
if((userData.petLevel < 7) || userData.canRevive || userData.canBreed) continue; if((userData.petLevel < 7) || userData.canRevive || userData.canBreed) continue;
} }
if(specialType === FurniCategory._Str_9449) if(specialType === FurniCategory.MONSTERPLANT_FERTILIZE)
{ {
if((userData.petLevel >= 7) || userData.canRevive) continue; if((userData.petLevel >= 7) || userData.canRevive) continue;
} }
@ -151,6 +167,11 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
if(useProductBubbles.length) this.container.eventDispatcher.dispatchEvent(new RoomWidgetUseProductBubbleEvent(RoomWidgetUseProductBubbleEvent.USE_PRODUCT_BUBBLES, useProductBubbles)); if(useProductBubbles.length) this.container.eventDispatcher.dispatchEvent(new RoomWidgetUseProductBubbleEvent(RoomWidgetUseProductBubbleEvent.USE_PRODUCT_BUBBLES, useProductBubbles));
} }
private processRoomSessionPetStatusUpdateEvent(event: RoomSessionPetStatusUpdateEvent): void
{
}
public get type(): string public get type(): string
{ {
return RoomWidgetEnum.AVATAR_INFO; return RoomWidgetEnum.AVATAR_INFO;
@ -162,18 +183,29 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
RoomSessionUserDataUpdateEvent.USER_DATA_UPDATED, RoomSessionUserDataUpdateEvent.USER_DATA_UPDATED,
RoomSessionDanceEvent.RSDE_DANCE, RoomSessionDanceEvent.RSDE_DANCE,
RoomEngineUseProductEvent.USE_PRODUCT_FROM_INVENTORY, RoomEngineUseProductEvent.USE_PRODUCT_FROM_INVENTORY,
RoomEngineUseProductEvent.USE_PRODUCT_FROM_ROOM RoomEngineUseProductEvent.USE_PRODUCT_FROM_ROOM,
RoomSessionPetStatusUpdateEvent.PET_STATUS_UPDATE
]; ];
} }
// UserNameUpdateEvent.UNUE_NAME_UPDATED
// RoomSessionNestBreedingSuccessEvent.RSPFUE_NEST_BREEDING_SUCCESS
// RoomSessionPetLevelUpdateEvent.RSPLUE_PET_LEVEL_UPDATE
public get messageTypes(): string[] public get messageTypes(): string[]
{ {
return [ return [
RoomWidgetRoomObjectMessage.GET_OWN_CHARACTER_INFO, RoomWidgetRoomObjectMessage.GET_OWN_CHARACTER_INFO,
RoomWidgetUserActionMessage.START_NAME_CHANGE,
RoomWidgetUserActionMessage.REQUEST_PET_UPDATE,
RoomWidgetUseProductMessage.PET_PRODUCT,
RoomWidgetUserActionMessage.REQUEST_BREED_PET,
RoomWidgetUserActionMessage.HARVEST_PET,
RoomWidgetUserActionMessage.REVIVE_PET,
RoomWidgetUserActionMessage.COMPOST_PLANT,
RoomWidgetDanceMessage.DANCE, RoomWidgetDanceMessage.DANCE,
RoomWidgetAvatarExpressionMessage.AVATAR_EXPRESSION, RoomWidgetAvatarExpressionMessage.AVATAR_EXPRESSION,
RoomWidgetChangePostureMessage.CHANGE_POSTURE, RoomWidgetChangePostureMessage.CHANGE_POSTURE,
RoomWidgetUseProductMessage.PET_PRODUCT
]; ];
} }
} }

View File

@ -1,9 +1,10 @@
import { AvatarExpressionEnum, HabboClubLevelEnum, NitroEvent, RoomControllerLevel, RoomSessionChatEvent, RoomSettingsComposer, RoomWidgetEnum, RoomZoomEvent, TextureUtils } from '@nitrots/nitro-renderer'; import { AvatarExpressionEnum, HabboClubLevelEnum, NitroEvent, RoomControllerLevel, RoomRotatingEffect, RoomSessionChatEvent, RoomSettingsComposer, RoomShakingEffect, RoomWidgetEnum, RoomZoomEvent, TextureUtils } from '@nitrots/nitro-renderer';
import { GetConfiguration, GetNitroInstance } from '../../..'; import { GetConfiguration, GetNitroInstance } from '../../..';
import { GetRoomEngine, GetSessionDataManager } from '../../../..'; import { GetRoomEngine, GetSessionDataManager, LocalizeText } from '../../../..';
import { FloorplanEditorEvent } from '../../../../../events/floorplan-editor/FloorplanEditorEvent'; import { FloorplanEditorEvent } from '../../../../../events/floorplan-editor/FloorplanEditorEvent';
import { dispatchUiEvent } from '../../../../../hooks'; import { dispatchUiEvent } from '../../../../../hooks';
import { SendMessageHook } from '../../../../../hooks/messages'; import { SendMessageHook } from '../../../../../hooks/messages';
import { NotificationUtilities } from '../../../../../views/notification-center/common/NotificationUtilities';
import { RoomWidgetFloodControlEvent, RoomWidgetUpdateEvent } from '../events'; import { RoomWidgetFloodControlEvent, RoomWidgetUpdateEvent } from '../events';
import { RoomWidgetChatMessage, RoomWidgetChatSelectAvatarMessage, RoomWidgetChatTypingMessage, RoomWidgetMessage, RoomWidgetRequestWidgetMessage } from '../messages'; import { RoomWidgetChatMessage, RoomWidgetChatSelectAvatarMessage, RoomWidgetChatTypingMessage, RoomWidgetMessage, RoomWidgetRequestWidgetMessage } from '../messages';
import { RoomWidgetHandler } from './RoomWidgetHandler'; import { RoomWidgetHandler } from './RoomWidgetHandler';
@ -65,6 +66,17 @@ export class RoomWidgetChatInputHandler extends RoomWidgetHandler
switch(firstPart.toLowerCase()) switch(firstPart.toLowerCase())
{ {
case ':shake':
RoomShakingEffect.init(2500, 5000);
RoomShakingEffect.turnVisualizationOn();
return null;
case ':rotate':
RoomRotatingEffect.init(2500, 5000);
RoomRotatingEffect.turnVisualizationOn();
return null;
case ':d': case ':d':
case ';d': case ';d':
if(GetSessionDataManager().clubLevel === HabboClubLevelEnum.VIP) if(GetSessionDataManager().clubLevel === HabboClubLevelEnum.VIP)
@ -109,6 +121,7 @@ export class RoomWidgetChatInputHandler extends RoomWidgetHandler
return null; return null;
case ':iddqd': case ':iddqd':
case ':flip':
GetRoomEngine().events.dispatchEvent(new RoomZoomEvent(this.container.roomSession.roomId, -1, true)); GetRoomEngine().events.dispatchEvent(new RoomZoomEvent(this.container.roomSession.roomId, -1, true));
return null; return null;
@ -127,10 +140,11 @@ export class RoomWidgetChatInputHandler extends RoomWidgetHandler
newWindow.document.write(image.outerHTML); newWindow.document.write(image.outerHTML);
return null; return null;
case ':pickall': case ':pickall':
// this.container.notificationService.alertWithConfirm('${room.confirm.pick_all}', '${generic.alert.title}', () => NotificationUtilities.confirm(LocalizeText('room.confirm.pick_all'), () =>
// { {
// GetSessionDataManager().sendSpecialCommandMessage(':pickall'); GetSessionDataManager().sendSpecialCommandMessage(':pickall');
// }); },
null, null, null, LocalizeText('generic.alert.title'));
return null; return null;
case ':furni': case ':furni':

View File

@ -1,4 +1,4 @@
import { IFurnitureData, NitroEvent, ObjectDataFactory, PetFigureData, PetRespectComposer, PetSupplementComposer, PetType, RoomControllerLevel, RoomModerationSettings, RoomObjectCategory, RoomObjectOperationType, RoomObjectType, RoomObjectVariable, RoomSessionPetInfoUpdateEvent, RoomSessionUserBadgesEvent, RoomTradingLevelEnum, RoomUnitDropHandItemComposer, RoomUnitGiveHandItemComposer, RoomUnitGiveHandItemPetComposer, RoomUserData, RoomWidgetEnum, RoomWidgetEnumItemExtradataParameter, Vector3d } from '@nitrots/nitro-renderer'; import { IFurnitureData, NitroEvent, ObjectDataFactory, PetFigureData, PetRespectComposer, PetSupplementComposer, PetType, RoomControllerLevel, RoomModerationSettings, RoomObjectCategory, RoomObjectOperationType, RoomObjectType, RoomObjectVariable, RoomSessionPetInfoUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, RoomTradingLevelEnum, RoomUnitDropHandItemComposer, RoomUnitGiveHandItemComposer, RoomUnitGiveHandItemPetComposer, RoomUserData, RoomWidgetEnum, RoomWidgetEnumItemExtradataParameter, Vector3d } from '@nitrots/nitro-renderer';
import { GetNitroInstance, GetRoomEngine, GetSessionDataManager, IsOwnerOfFurniture } from '../../../..'; import { GetNitroInstance, GetRoomEngine, GetSessionDataManager, IsOwnerOfFurniture } from '../../../..';
import { InventoryTradeRequestEvent, WiredSelectObjectEvent } from '../../../../../events'; import { InventoryTradeRequestEvent, WiredSelectObjectEvent } from '../../../../../events';
import { FriendsSendFriendRequestEvent } from '../../../../../events/friends/FriendsSendFriendRequestEvent'; import { FriendsSendFriendRequestEvent } from '../../../../../events/friends/FriendsSendFriendRequestEvent';
@ -24,6 +24,9 @@ export class RoomWidgetInfostandHandler extends RoomWidgetHandler
case RoomSessionUserBadgesEvent.RSUBE_BADGES: case RoomSessionUserBadgesEvent.RSUBE_BADGES:
this.container.eventDispatcher.dispatchEvent(event); this.container.eventDispatcher.dispatchEvent(event);
return; return;
case RoomSessionUserFigureUpdateEvent.USER_FIGURE:
this.processRoomSessionUserFigureUpdateEvent((event as RoomSessionUserFigureUpdateEvent));
return;
} }
} }
@ -661,6 +664,17 @@ export class RoomWidgetInfostandHandler extends RoomWidgetHandler
this.container.eventDispatcher.dispatchEvent(infostandEvent); this.container.eventDispatcher.dispatchEvent(infostandEvent);
} }
private processRoomSessionUserFigureUpdateEvent(event: RoomSessionUserFigureUpdateEvent): void
{
const userData = this.container.roomSession.userDataManager.getUserDataByIndex(event.userId);
if(!userData) return;
// update active infostand figure
// update motto
// update activity points
}
private checkGuildSetting(event: RoomWidgetUpdateInfostandUserEvent): boolean private checkGuildSetting(event: RoomWidgetUpdateInfostandUserEvent): boolean
{ {
if(event.isGuildRoom) return (event.roomControllerLevel >= RoomControllerLevel.GUILD_ADMIN); if(event.isGuildRoom) return (event.roomControllerLevel >= RoomControllerLevel.GUILD_ADMIN);
@ -766,7 +780,8 @@ export class RoomWidgetInfostandHandler extends RoomWidgetHandler
{ {
return [ return [
RoomSessionPetInfoUpdateEvent.PET_INFO, RoomSessionPetInfoUpdateEvent.PET_INFO,
RoomSessionUserBadgesEvent.RSUBE_BADGES RoomSessionUserBadgesEvent.RSUBE_BADGES,
RoomSessionUserFigureUpdateEvent.USER_FIGURE
]; ];
} }

View File

@ -1,12 +1,12 @@
import { IFurnitureData } from '@nitrots/nitro-renderer'; import { IFurnitureData } from '@nitrots/nitro-renderer';
import { GetSessionDataManager } from '.'; import { GetSessionDataManager } from '.';
import { ProductTypeEnum } from '../../../views/catalog/common/ProductTypeEnum'; import { ProductTypeEnum } from '../../../components/catalog/common/ProductTypeEnum';
export function GetFurnitureData(furniClassId: number, productType: string): IFurnitureData export function GetFurnitureData(furniClassId: number, productType: string): IFurnitureData
{ {
let furniData: IFurnitureData = null; let furniData: IFurnitureData = null;
switch(productType.toUpperCase()) switch(productType.toLowerCase())
{ {
case ProductTypeEnum.FLOOR: case ProductTypeEnum.FLOOR:
furniData = GetSessionDataManager().getFloorItemData(furniClassId); furniData = GetSessionDataManager().getFloorItemData(furniClassId);

View File

@ -0,0 +1,16 @@
import { RoomObjectCategory, RoomObjectVariable } from '@nitrots/nitro-renderer';
import { GetRoomSession } from '.';
import { GetRoomEngine } from '..';
import { GetSessionDataManager } from '../../../api';
export function IsOwnerOfFloorFurniture(id: number): boolean
{
const roomObject = GetRoomEngine().getRoomObject(GetRoomSession().roomId, id, RoomObjectCategory.FLOOR);
if(!roomObject || !roomObject.model) return false;
const userId = GetSessionDataManager().userId;
const objectOwnerId = roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_ID);
return (userId === objectOwnerId);
}

View File

@ -14,6 +14,7 @@ export * from './GetSessionDataManager';
export * from './GoToDesktop'; export * from './GoToDesktop';
export * from './HasHabboClub'; export * from './HasHabboClub';
export * from './HasHabboVip'; export * from './HasHabboVip';
export * from './IsOwnerOfFloorFurniture';
export * from './IsOwnerOfFurniture'; export * from './IsOwnerOfFurniture';
export * from './IsRidingHorse'; export * from './IsRidingHorse';
export * from './StartRoomSession'; export * from './StartRoomSession';

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

View File

@ -79,6 +79,11 @@
font-weight: $font-weight-normal; font-weight: $font-weight-normal;
color: $btn-link-color; color: $btn-link-color;
text-decoration: $link-decoration; text-decoration: $link-decoration;
box-shadow: none !important;
&:active {
color: $btn-link-color !important;
}
&:hover { &:hover {
color: $btn-link-hover-color; color: $btn-link-hover-color;

View File

@ -4,34 +4,37 @@
// `<nav>`s, `<ul>`s or `<ol>`s. // `<nav>`s, `<ul>`s or `<ol>`s.
.nav { .nav {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
padding-left: 0; padding-left: 0;
margin-bottom: 0; margin-bottom: 0;
list-style: none; list-style: none;
} }
.nav-link { .nav-link {
display: block; display: block;
padding: $nav-link-padding-y $nav-link-padding-x; padding: $nav-link-padding-y $nav-link-padding-x;
@include font-size($nav-link-font-size); @include font-size($nav-link-font-size);
font-weight: $nav-link-font-weight; font-weight: $nav-link-font-weight;
color: $nav-link-color; color: $nav-link-color;
text-decoration: if($link-decoration == none, null, none); text-decoration: if($link-decoration == none, null, none);
@include transition($nav-link-transition); @include transition($nav-link-transition);
&:hover, display: flex;
&:focus { align-items: center;
color: $nav-link-hover-color;
text-decoration: if($link-hover-decoration == underline, none, null);
}
// Disabled state lightens text &:hover,
&.disabled { &:focus {
color: $nav-link-disabled-color; color: $nav-link-hover-color;
pointer-events: none; text-decoration: if($link-hover-decoration == underline, none, null);
cursor: default; }
}
// Disabled state lightens text
&.disabled {
color: $nav-link-disabled-color;
pointer-events: none;
cursor: default;
}
} }
// //
@ -39,122 +42,119 @@
// //
.nav-tabs { .nav-tabs {
border-bottom: $nav-tabs-border-width solid $nav-tabs-border-color; border-bottom: $nav-tabs-border-width solid $nav-tabs-border-color;
.nav-link { .nav-link {
position: relative; position: relative;
margin-bottom: -$nav-tabs-border-width; margin-bottom: -$nav-tabs-border-width;
background-color: $nav-tabs-link-bg; background-color: $nav-tabs-link-bg;
border: $nav-tabs-border-width solid $nav-tabs-border-color; border: $nav-tabs-border-width solid $nav-tabs-border-color;
@include border-top-radius($nav-tabs-border-radius); @include border-top-radius($nav-tabs-border-radius);
&.active { &.active {
&:before { &:before {
background: #FFFFFF; background: #ffffff;
} }
}
&:before {
content: "";
position: absolute;
width: 93%;
height: 3px;
border-radius: $border-radius;
top: 1.5px;
left: 0;
right: 0;
margin: auto;
background: #c2c9d1;
z-index: 1;
}
&:hover,
&:focus {
border-color: $nav-tabs-link-hover-border-color;
// Prevents active .nav-link tab overlapping focus outline of previous/next .nav-link
isolation: isolate;
}
&.disabled {
color: $nav-link-disabled-color;
background-color: transparent;
border-color: transparent;
}
} }
&:before { .nav-link.active,
content: ""; .nav-item.show .nav-link {
position: absolute; color: $nav-tabs-link-active-color;
width: 93%; background-color: $nav-tabs-link-active-bg;
height: 3px; border-color: $nav-tabs-link-active-border-color;
border-radius: $border-radius;
top: 1.5px;
left: 0;
right: 0;
margin: auto;
background: #C2C9D1;
z-index: 1;
} }
&:hover, .dropdown-menu {
&:focus { // Make dropdown border overlap tab border
border-color: $nav-tabs-link-hover-border-color; margin-top: -$nav-tabs-border-width;
// Prevents active .nav-link tab overlapping focus outline of previous/next .nav-link // Remove the top rounded corners here since there is a hard edge above the menu
isolation: isolate; @include border-top-radius(0);
} }
&.disabled {
color: $nav-link-disabled-color;
background-color: transparent;
border-color: transparent;
}
}
.nav-link.active,
.nav-item.show .nav-link {
color: $nav-tabs-link-active-color;
background-color: $nav-tabs-link-active-bg;
border-color: $nav-tabs-link-active-border-color;
}
.dropdown-menu {
// Make dropdown border overlap tab border
margin-top: -$nav-tabs-border-width;
// Remove the top rounded corners here since there is a hard edge above the menu
@include border-top-radius(0);
}
} }
// //
// Pills // Pills
// //
.nav-pills { .nav-pills {
.nav-link { .nav-link {
background: none; background: none;
border: 0; border: 0;
@include border-radius($nav-pills-border-radius); @include border-radius($nav-pills-border-radius);
} }
.nav-link.active, .nav-link.active,
.show > .nav-link { .show > .nav-link {
color: $nav-pills-link-active-color; color: $nav-pills-link-active-color;
@include gradient-bg($nav-pills-link-active-bg); @include gradient-bg($nav-pills-link-active-bg);
} }
} }
// //
// Justified variants // Justified variants
// //
.nav-fill { .nav-fill {
> .nav-link, > .nav-link,
.nav-item { .nav-item {
flex: 1 1 auto; flex: 1 1 auto;
text-align: center; text-align: center;
} }
} }
.nav-justified { .nav-justified {
> .nav-link, > .nav-link,
.nav-item { .nav-item {
flex-basis: 0; flex-basis: 0;
flex-grow: 1; flex-grow: 1;
text-align: center; text-align: center;
} }
} }
.nav-fill, .nav-fill,
.nav-justified { .nav-justified {
.nav-item .nav-link { .nav-item .nav-link {
width: 100%; // Make sure button will grow width: 100%; // Make sure button will grow
} }
} }
// Tabbable tabs // Tabbable tabs
// //
// Hide tabbable panes to start, show them when `.active` // Hide tabbable panes to start, show them when `.active`
.tab-content { .tab-content {
> .tab-pane { > .tab-pane {
display: none; display: none;
} }
> .active { > .active {
display: block; display: block;
} }
} }

View File

@ -1,6 +1,5 @@
// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix // stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix
// Reboot // Reboot
// //
// Normalization of HTML elements, manually forked from Normalize.css to remove // Normalization of HTML elements, manually forked from Normalize.css to remove
@ -8,7 +7,6 @@
// //
// Normalize is licensed MIT. https://github.com/necolas/normalize.css // Normalize is licensed MIT. https://github.com/necolas/normalize.css
// Document // Document
// //
// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`. // Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.
@ -16,27 +14,26 @@
*, *,
*::before, *::before,
*::after { *::after {
box-sizing: border-box; line-height: normal;
box-sizing: border-box;
} }
// Root // Root
// //
// Ability to the value of the root font sizes, affecting the value of `rem`. // Ability to the value of the root font sizes, affecting the value of `rem`.
// null by default, thus nothing is generated. // null by default, thus nothing is generated.
:root { :root {
@if $font-size-root != null { @if $font-size-root != null {
font-size: var(--#{$variable-prefix}-root-font-size); font-size: var(--#{$variable-prefix}-root-font-size);
}
@if $enable-smooth-scroll {
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
} }
}
}
@if $enable-smooth-scroll {
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
}
}
// Body // Body
// //
@ -47,38 +44,36 @@
// scss-docs-start reboot-body-rules // scss-docs-start reboot-body-rules
body { body {
margin: 0; // 1 margin: 0; // 1
font-family: var(--#{$variable-prefix}body-font-family); font-family: var(--#{$variable-prefix}body-font-family);
@include font-size(var(--#{$variable-prefix}body-font-size)); @include font-size(var(--#{$variable-prefix}body-font-size));
font-weight: var(--#{$variable-prefix}body-font-weight); font-weight: var(--#{$variable-prefix}body-font-weight);
line-height: var(--#{$variable-prefix}body-line-height); line-height: var(--#{$variable-prefix}body-line-height);
color: var(--#{$variable-prefix}body-color); color: var(--#{$variable-prefix}body-color);
text-align: var(--#{$variable-prefix}body-text-align); text-align: var(--#{$variable-prefix}body-text-align);
background-color: var(--#{$variable-prefix}body-bg); // 2 background-color: var(--#{$variable-prefix}body-bg); // 2
-webkit-text-size-adjust: 100%; // 3 -webkit-text-size-adjust: 100%; // 3
-webkit-tap-highlight-color: rgba($black, 0); // 4 -webkit-tap-highlight-color: rgba($black, 0); // 4
} }
// scss-docs-end reboot-body-rules // scss-docs-end reboot-body-rules
// Content grouping // Content grouping
// //
// 1. Reset Firefox's gray color // 1. Reset Firefox's gray color
// 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field // 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field
hr { hr {
margin: $hr-margin-y 0; margin: $hr-margin-y 0;
color: $hr-color; // 1 color: $hr-color; // 1
background-color: currentColor; background-color: currentColor;
border: 0; border: 0;
opacity: $hr-opacity; opacity: $hr-opacity;
} }
hr:not([size]) { hr:not([size]) {
height: $hr-height; // 2 height: $hr-height; // 2
} }
// Typography // Typography
// //
// 1. Remove top margins from headings // 1. Remove top margins from headings
@ -86,57 +81,55 @@ hr:not([size]) {
// margin for easier control within type scales as it avoids margin collapsing. // margin for easier control within type scales as it avoids margin collapsing.
%heading { %heading {
margin-top: 0; // 1 margin-top: 0; // 1
margin-bottom: $headings-margin-bottom; margin-bottom: $headings-margin-bottom;
font-family: $headings-font-family; font-family: $headings-font-family;
font-style: $headings-font-style; font-style: $headings-font-style;
font-weight: $headings-font-weight; font-weight: $headings-font-weight;
line-height: $headings-line-height; line-height: $headings-line-height;
color: $headings-color; color: $headings-color;
} }
h1 { h1 {
@extend %heading; @extend %heading;
@include font-size($h1-font-size); @include font-size($h1-font-size);
} }
h2 { h2 {
@extend %heading; @extend %heading;
@include font-size($h2-font-size); @include font-size($h2-font-size);
} }
h3 { h3 {
@extend %heading; @extend %heading;
@include font-size($h3-font-size); @include font-size($h3-font-size);
} }
h4 { h4 {
@extend %heading; @extend %heading;
@include font-size($h4-font-size); @include font-size($h4-font-size);
} }
h5 { h5 {
@extend %heading; @extend %heading;
@include font-size($h5-font-size); @include font-size($h5-font-size);
} }
h6 { h6 {
@extend %heading; @extend %heading;
@include font-size($h6-font-size); @include font-size($h6-font-size);
} }
// Reset margins on paragraphs // Reset margins on paragraphs
// //
// Similarly, the top margin on `<p>`s get reset. However, we also reset the // Similarly, the top margin on `<p>`s get reset. However, we also reset the
// bottom margin to use `rem` units instead of `em`. // bottom margin to use `rem` units instead of `em`.
p { p {
margin-top: 0; margin-top: 0;
margin-bottom: $paragraph-margin-bottom; margin-bottom: $paragraph-margin-bottom;
} }
// Abbreviations // Abbreviations
// //
// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin // 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin
@ -145,89 +138,83 @@ p {
// 4. Prevent the text-decoration to be skipped. // 4. Prevent the text-decoration to be skipped.
abbr[title], abbr[title],
abbr[data-bs-original-title] { // 1 abbr[data-bs-original-title] {
text-decoration: underline dotted; // 2 // 1
cursor: help; // 3 text-decoration: underline dotted; // 2
text-decoration-skip-ink: none; // 4 cursor: help; // 3
text-decoration-skip-ink: none; // 4
} }
// Address // Address
address { address {
margin-bottom: 1rem; margin-bottom: 1rem;
font-style: normal; font-style: normal;
line-height: inherit; line-height: inherit;
} }
// Lists // Lists
ol, ol,
ul { ul {
padding-left: 2rem; padding-left: 2rem;
} }
ol, ol,
ul, ul,
dl { dl {
margin-top: 0; margin-top: 0;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
ol ol, ol ol,
ul ul, ul ul,
ol ul, ol ul,
ul ol { ul ol {
margin-bottom: 0; margin-bottom: 0;
} }
dt { dt {
font-weight: $dt-font-weight; font-weight: $dt-font-weight;
} }
// 1. Undo browser default // 1. Undo browser default
dd { dd {
margin-bottom: .5rem; margin-bottom: 0.5rem;
margin-left: 0; // 1 margin-left: 0; // 1
} }
// Blockquote // Blockquote
blockquote { blockquote {
margin: 0 0 1rem; margin: 0 0 1rem;
} }
// Strong // Strong
// //
// Add the correct font weight in Chrome, Edge, and Safari // Add the correct font weight in Chrome, Edge, and Safari
b, b,
strong { strong {
font-weight: $font-weight-bolder; font-weight: $font-weight-bolder;
} }
// Small // Small
// //
// Add the correct font size in all browsers // Add the correct font size in all browsers
small { small {
@include font-size($small-font-size); @include font-size($small-font-size);
} }
// Mark // Mark
mark { mark {
padding: $mark-padding; padding: $mark-padding;
background-color: $mark-bg; background-color: $mark-bg;
} }
// Sub and Sup // Sub and Sup
// //
// Prevent `sub` and `sup` elements from affecting the line height in // Prevent `sub` and `sup` elements from affecting the line height in
@ -235,26 +222,29 @@ mark {
sub, sub,
sup { sup {
position: relative; position: relative;
@include font-size($sub-sup-font-size); @include font-size($sub-sup-font-size);
line-height: 0; line-height: 0;
vertical-align: baseline; vertical-align: baseline;
} }
sub { bottom: -.25em; } sub {
sup { top: -.5em; } bottom: -0.25em;
}
sup {
top: -0.5em;
}
// Links // Links
a { a {
color: $link-color; color: $link-color;
text-decoration: $link-decoration; text-decoration: $link-decoration;
&:hover { &:hover {
color: $link-hover-color; color: $link-hover-color;
text-decoration: $link-hover-decoration; text-decoration: $link-hover-decoration;
} }
} }
// And undo these styles for placeholder links/named anchors (without href). // And undo these styles for placeholder links/named anchors (without href).
@ -263,24 +253,25 @@ a {
// See https://github.com/twbs/bootstrap/issues/19402 // See https://github.com/twbs/bootstrap/issues/19402
a:not([href]):not([class]) { a:not([href]):not([class]) {
&, &,
&:hover { &:hover {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
} }
} }
// Code // Code
pre, pre,
code, code,
kbd, kbd,
samp { samp {
font-family: $font-family-code; font-family: $font-family-code;
@include font-size(1em); // Correct the odd `em` font sizing in all browsers. @include font-size(
direction: ltr #{"/* rtl:ignore */"}; 1em
unicode-bidi: bidi-override; ); // Correct the odd `em` font sizing in all browsers.
direction: ltr #{"/* rtl:ignore */"};
unicode-bidi: bidi-override;
} }
// 1. Remove browser default top margin // 1. Remove browser default top margin
@ -288,78 +279,75 @@ samp {
// 3. Don't allow content to break outside // 3. Don't allow content to break outside
pre { pre {
display: block; display: block;
margin-top: 0; // 1 margin-top: 0; // 1
margin-bottom: 1rem; // 2 margin-bottom: 1rem; // 2
overflow: auto; // 3 overflow: auto; // 3
@include font-size($code-font-size); @include font-size($code-font-size);
color: $pre-color; color: $pre-color;
// Account for some code outputs that place code tags in pre tags // Account for some code outputs that place code tags in pre tags
code { code {
@include font-size(inherit); @include font-size(inherit);
color: inherit; color: inherit;
word-break: normal; word-break: normal;
} }
} }
code { code {
@include font-size($code-font-size); @include font-size($code-font-size);
color: $code-color; color: $code-color;
word-wrap: break-word; word-wrap: break-word;
// Streamline the style when inside anchors to avoid broken underline and more // Streamline the style when inside anchors to avoid broken underline and more
a > & { a > & {
color: inherit; color: inherit;
} }
} }
kbd { kbd {
padding: $kbd-padding-y $kbd-padding-x; padding: $kbd-padding-y $kbd-padding-x;
@include font-size($kbd-font-size); @include font-size($kbd-font-size);
color: $kbd-color; color: $kbd-color;
background-color: $kbd-bg; background-color: $kbd-bg;
@include border-radius($border-radius-sm); @include border-radius($border-radius-sm);
kbd { kbd {
padding: 0; padding: 0;
@include font-size(1em); @include font-size(1em);
font-weight: $nested-kbd-font-weight; font-weight: $nested-kbd-font-weight;
} }
} }
// Figures // Figures
// //
// Apply a consistent margin strategy (matches our type styles). // Apply a consistent margin strategy (matches our type styles).
figure { figure {
margin: 0 0 1rem; margin: 0 0 1rem;
} }
// Images and content // Images and content
img, img,
svg { svg {
vertical-align: middle; vertical-align: middle;
} }
// Tables // Tables
// //
// Prevent double borders // Prevent double borders
table { table {
caption-side: bottom; caption-side: bottom;
border-collapse: collapse; border-collapse: collapse;
} }
caption { caption {
padding-top: $table-cell-padding-y; padding-top: $table-cell-padding-y;
padding-bottom: $table-cell-padding-y; padding-bottom: $table-cell-padding-y;
color: $table-caption-color; color: $table-caption-color;
text-align: left; text-align: left;
} }
// 1. Removes font-weight bold by inheriting // 1. Removes font-weight bold by inheriting
@ -367,9 +355,9 @@ caption {
// 3. Fix alignment for Safari // 3. Fix alignment for Safari
th { th {
font-weight: $table-th-font-weight; // 1 font-weight: $table-th-font-weight; // 1
text-align: inherit; // 2 text-align: inherit; // 2
text-align: -webkit-match-parent; // 3 text-align: -webkit-match-parent; // 3
} }
thead, thead,
@ -378,26 +366,25 @@ tfoot,
tr, tr,
td, td,
th { th {
border-color: inherit; border-color: inherit;
border-style: solid; border-style: solid;
border-width: 0; border-width: 0;
} }
// Forms // Forms
// //
// 1. Allow labels to use `margin` for spacing. // 1. Allow labels to use `margin` for spacing.
label { label {
display: inline-block; // 1 display: inline-block; // 1
} }
// Remove the default `border-radius` that macOS Chrome adds. // Remove the default `border-radius` that macOS Chrome adds.
// See https://github.com/twbs/bootstrap/issues/24093 // See https://github.com/twbs/bootstrap/issues/24093
button { button {
// stylelint-disable-next-line property-disallowed-list // stylelint-disable-next-line property-disallowed-list
border-radius: 0; border-radius: 0;
} }
// Explicitly remove focus outline in Chromium when it shouldn't be // Explicitly remove focus outline in Chromium when it shouldn't be
@ -406,7 +393,7 @@ button {
// confused and applies its very visible two-tone outline anyway. // confused and applies its very visible two-tone outline anyway.
button:focus:not(:focus-visible) { button:focus:not(:focus-visible) {
outline: 0; outline: 0;
} }
// 1. Remove the margin in Firefox and Safari // 1. Remove the margin in Firefox and Safari
@ -416,40 +403,40 @@ button,
select, select,
optgroup, optgroup,
textarea { textarea {
margin: 0; // 1 margin: 0; // 1
font-family: inherit; font-family: inherit;
@include font-size(inherit); @include font-size(inherit);
line-height: inherit; line-height: inherit;
} }
// Remove the inheritance of text transform in Firefox // Remove the inheritance of text transform in Firefox
button, button,
select { select {
text-transform: none; text-transform: none;
} }
// Set the cursor for non-`<button>` buttons // Set the cursor for non-`<button>` buttons
// //
// Details at https://github.com/twbs/bootstrap/pull/30562 // Details at https://github.com/twbs/bootstrap/pull/30562
[role="button"] { [role="button"] {
cursor: pointer; cursor: pointer;
} }
select { select {
// Remove the inheritance of word-wrap in Safari. // Remove the inheritance of word-wrap in Safari.
// See https://github.com/twbs/bootstrap/issues/24990 // See https://github.com/twbs/bootstrap/issues/24990
word-wrap: normal; word-wrap: normal;
// Undo the opacity change from Chrome // Undo the opacity change from Chrome
&:disabled { &:disabled {
opacity: 1; opacity: 1;
} }
} }
// Remove the dropdown arrow in Chrome from inputs built with datalists. // Remove the dropdown arrow in Chrome from inputs built with datalists.
// See https://stackoverflow.com/a/54997118 // See https://stackoverflow.com/a/54997118
[list]::-webkit-calendar-picker-indicator { [list]::-webkit-calendar-picker-indicator {
display: none; display: none;
} }
// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` // 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
@ -461,26 +448,26 @@ button,
[type="button"], // 1 [type="button"], // 1
[type="reset"], [type="reset"],
[type="submit"] { [type="submit"] {
-webkit-appearance: button; // 2 -webkit-appearance: button; // 2
@if $enable-button-pointers { @if $enable-button-pointers {
&:not(:disabled) { &:not(:disabled) {
cursor: pointer; // 3 cursor: pointer; // 3
}
} }
}
} }
// Remove inner border and padding from Firefox, but don't restore the outline like Normalize. // Remove inner border and padding from Firefox, but don't restore the outline like Normalize.
::-moz-focus-inner { ::-moz-focus-inner {
padding: 0; padding: 0;
border-style: none; border-style: none;
} }
// 1. Textareas should really only resize vertically so they don't break their (horizontal) containers. // 1. Textareas should really only resize vertically so they don't break their (horizontal) containers.
textarea { textarea {
resize: vertical; // 1 resize: vertical; // 1
} }
// 1. Browsers set a default `min-width: min-content;` on fieldsets, // 1. Browsers set a default `min-width: min-content;` on fieldsets,
@ -491,10 +478,10 @@ textarea {
// 2. Reset the default outline behavior of fieldsets so they don't affect page layout. // 2. Reset the default outline behavior of fieldsets so they don't affect page layout.
fieldset { fieldset {
min-width: 0; // 1 min-width: 0; // 1
padding: 0; // 2 padding: 0; // 2
margin: 0; // 2 margin: 0; // 2
border: 0; // 2 border: 0; // 2
} }
// 1. By using `float: left`, the legend will behave like a block element. // 1. By using `float: left`, the legend will behave like a block element.
@ -503,17 +490,17 @@ fieldset {
// See https://github.com/twbs/bootstrap/issues/29712 // See https://github.com/twbs/bootstrap/issues/29712
legend { legend {
float: left; // 1 float: left; // 1
width: 100%; width: 100%;
padding: 0; padding: 0;
margin-bottom: $legend-margin-bottom; margin-bottom: $legend-margin-bottom;
@include font-size($legend-font-size); @include font-size($legend-font-size);
font-weight: $legend-font-weight; font-weight: $legend-font-weight;
line-height: inherit; line-height: inherit;
+ * { + * {
clear: left; // 2 clear: left; // 2
} }
} }
// Fix height of inputs with a type of datetime-local, date, month, week, or time // Fix height of inputs with a type of datetime-local, date, month, week, or time
@ -526,11 +513,11 @@ legend {
::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field { ::-webkit-datetime-edit-year-field {
padding: 0; padding: 0;
} }
::-webkit-inner-spin-button { ::-webkit-inner-spin-button {
height: auto; height: auto;
} }
// 1. Correct the outline style in Safari. // 1. Correct the outline style in Safari.
@ -540,8 +527,8 @@ legend {
// https://github.com/twbs/bootstrap/issues/11586. // https://github.com/twbs/bootstrap/issues/11586.
[type="search"] { [type="search"] {
outline-offset: -2px; // 1 outline-offset: -2px; // 1
-webkit-appearance: textfield; // 2 -webkit-appearance: textfield; // 2
} }
// 1. A few input types should stay LTR // 1. A few input types should stay LTR
@ -561,40 +548,39 @@ legend {
// Remove the inner padding in Chrome and Safari on macOS. // Remove the inner padding in Chrome and Safari on macOS.
::-webkit-search-decoration { ::-webkit-search-decoration {
-webkit-appearance: none; -webkit-appearance: none;
} }
// Remove padding around color pickers in webkit browsers // Remove padding around color pickers in webkit browsers
::-webkit-color-swatch-wrapper { ::-webkit-color-swatch-wrapper {
padding: 0; padding: 0;
} }
// Inherit font family and line height for file input buttons // Inherit font family and line height for file input buttons
::file-selector-button { ::file-selector-button {
font: inherit; font: inherit;
} }
// 1. Change font properties to `inherit` // 1. Change font properties to `inherit`
// 2. Correct the inability to style clickable types in iOS and Safari. // 2. Correct the inability to style clickable types in iOS and Safari.
::-webkit-file-upload-button { ::-webkit-file-upload-button {
font: inherit; // 1 font: inherit; // 1
-webkit-appearance: button; // 2 -webkit-appearance: button; // 2
} }
// Correct element displays // Correct element displays
output { output {
display: inline-block; display: inline-block;
} }
// Remove border from iframe // Remove border from iframe
iframe { iframe {
border: 0; border: 0;
} }
// Summary // Summary
@ -602,24 +588,22 @@ iframe {
// 1. Add the correct display in all browsers // 1. Add the correct display in all browsers
summary { summary {
display: list-item; // 1 display: list-item; // 1
cursor: pointer; cursor: pointer;
} }
// Progress // Progress
// //
// Add the correct vertical alignment in Chrome, Firefox, and Opera. // Add the correct vertical alignment in Chrome, Firefox, and Opera.
progress { progress {
vertical-align: baseline; vertical-align: baseline;
} }
// Hidden attribute // Hidden attribute
// //
// Always hide an element with the `hidden` HTML attribute. // Always hide an element with the `hidden` HTML attribute.
[hidden] { [hidden] {
display: none !important; display: none !important;
} }

View File

@ -390,7 +390,7 @@ $enable-cssgrid: true !default;
$enable-button-pointers: true !default; $enable-button-pointers: true !default;
$enable-rfs: true !default; $enable-rfs: true !default;
$enable-validation-icons: true !default; $enable-validation-icons: true !default;
$enable-negative-margins: false !default; $enable-negative-margins: true !default;
$enable-deprecation-messages: true !default; $enable-deprecation-messages: true !default;
$enable-important-utilities: true !default; $enable-important-utilities: true !default;
@ -732,7 +732,7 @@ $table-cell-padding-x-sm: .25rem !default;
$table-cell-vertical-align: top !default; $table-cell-vertical-align: top !default;
$table-color: $body-color !default; $table-color: $black !default;
$table-bg: transparent !default; $table-bg: transparent !default;
$table-accent-bg: transparent !default; $table-accent-bg: transparent !default;

View File

@ -17,7 +17,7 @@
.form-check-input { .form-check-input {
width: $form-check-input-width; width: $form-check-input-width;
height: $form-check-input-width; height: $form-check-input-width;
margin-top: ($line-height-base - $form-check-input-width) * .5; // line-height minus check height //margin-top: ($line-height-base - $form-check-input-width) * .5; // line-height minus check height
vertical-align: top; vertical-align: top;
background-color: $form-check-input-bg; background-color: $form-check-input-bg;
background-repeat: no-repeat; background-repeat: no-repeat;

View File

@ -1,4 +1,5 @@
.fas { .fas,
.svg-inline--fa {
line-height: 0 !important; line-height: 0 !important;
} }
@ -303,6 +304,48 @@
height: 15px; height: 15px;
} }
&.icon-small-room {
background: url("../images/icons/small-room.png");
width: 17px;
height: 16px;
}
&.icon-cog {
background: url("../images/icons/cog.png");
width: 21px;
height: 21px;
}
&.icon-chat-history {
background: url("../images/icons/chat-history.png");
width: 17px;
height: 21px;
}
&.icon-room-link {
background: url("../images/icons/room-link.png");
width: 17px;
height: 15px;
}
&.icon-zoom-more {
background: url("../images/icons/zoom-more.png");
width: 12px;
height: 22px;
}
&.icon-zoom-less {
background: url("../images/icons/zoom-less.png");
width: 12px;
height: 22px;
}
&.icon-like-room {
background: url("../images/icons/like-room.png");
width: 20px;
height: 22px;
}
&.icon-arrows { &.icon-arrows {
background: url("../images/icons/arrows.png"); background: url("../images/icons/arrows.png");
width: 17px; width: 17px;

View File

@ -1,9 +1,5 @@
@import './fonts'; @import './fonts';
@import './bootstrap/bootstrap'; @import './bootstrap/bootstrap';
@import './fontawesome/fontawesome';
@import './fontawesome/solid';
@import './fontawesome/brands';
@import './fontawesome/regular';
@import '../node_modules/animate.css/animate.min.css'; @import '../node_modules/animate.css/animate.min.css';
@import './scrollbars'; @import './scrollbars';
@import './slider'; @import './slider';

View File

@ -47,6 +47,10 @@ ul {
cursor: pointer; cursor: pointer;
} }
.cursor-not-allowed {
cursor: not-allowed;
}
.pointer-events-none { .pointer-events-none {
pointer-events: none; pointer-events: none;
} }
@ -78,3 +82,18 @@ ul {
.z-index-1 { .z-index-1 {
z-index: 1; z-index: 1;
} }
.flex-basis-fit-content {
flex-basis: fit-content;
}
.flex-basis-max-content {
flex-basis: max-content;
}
.striped-children {
> :nth-child(1) {
background-color: $table-striped-bg;
}
}

28
src/common/AutoGrid.tsx Normal file
View File

@ -0,0 +1,28 @@
import { CSSProperties, FC, useMemo } from 'react';
import { Grid, GridProps } from './Grid';
export interface AutoGridProps extends GridProps
{
columnMinWidth?: number;
columnMinHeight?: number;
}
export const AutoGrid: FC<AutoGridProps> = props =>
{
const { columnMinWidth = 40, columnMinHeight = 40, columnCount = 0, fullHeight = false, maxContent = true, overflow = 'auto', style = {}, ...rest } = props;
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
newStyle['--nitro-grid-column-min-height'] = (columnMinHeight + 'px');
if(columnCount > 1) newStyle.gridTemplateColumns = `repeat(auto-fill, minmax(${ columnMinWidth }px, 1fr))`;
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ columnMinWidth, columnMinHeight, columnCount, style ]);
return <Grid columnCount={ columnCount } fullHeight={ fullHeight } overflow={ overflow } style={ getStyle } { ...rest } />;
}

70
src/common/Base.tsx Normal file
View File

@ -0,0 +1,70 @@
import { CSSProperties, DetailedHTMLProps, FC, HTMLAttributes, LegacyRef, useMemo } from 'react';
import { ColorVariantType, FloatType, OverflowType, PositionType } from './types';
export interface BaseProps<T = HTMLElement> extends DetailedHTMLProps<HTMLAttributes<T>, T>
{
innerRef?: LegacyRef<T>;
fit?: boolean;
grow?: boolean;
shrink?: boolean;
fullWidth?: boolean;
fullHeight?: boolean;
overflow?: OverflowType;
position?: PositionType;
float?: FloatType;
pointer?: boolean;
textColor?: ColorVariantType;
classNames?: string[];
}
export const Base: FC<BaseProps<HTMLDivElement>> = props =>
{
const { ref = null, innerRef = null, fit = false, grow = false, shrink = false, fullWidth = false, fullHeight = false, overflow = null, position = null, float = null, pointer = false, textColor = null, classNames = [], className = '', style = {}, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [];
if(fit || fullWidth) newClassNames.push('w-100');
if(fit || fullHeight) newClassNames.push('h-100');
if(grow) newClassNames.push('flex-grow-1');
if(shrink) newClassNames.push('flex-shrink-0');
if(overflow) newClassNames.push('overflow-' + overflow);
if(position) newClassNames.push('position-' + position);
if(float) newClassNames.push('float-' + float);
if(pointer) newClassNames.push('cursor-pointer');
if(textColor) newClassNames.push('text-' + textColor);
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ fit, grow, shrink, fullWidth, fullHeight, overflow, position, float, pointer, textColor, classNames ]);
const getClassName = useMemo(() =>
{
let newClassName = getClassNames.join(' ');
if(className.length) newClassName += (' ' + className);
return newClassName.trim();
}, [ getClassNames, className ]);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ style ]);
return <div ref={ innerRef } className={ getClassName } style={ getStyle } { ...rest } />;
}

35
src/common/Button.tsx Normal file
View File

@ -0,0 +1,35 @@
import { FC, useMemo } from 'react';
import { Flex, FlexProps } from './Flex';
import { ButtonSizeType, ColorVariantType } from './types';
export interface ButtonProps extends FlexProps
{
variant?: ColorVariantType;
size?: ButtonSizeType;
active?: boolean;
disabled?: boolean;
}
export const Button: FC<ButtonProps> = props =>
{
const { variant = 'primary', size = 'sm', active = false, disabled = false, classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'btn' ];
if(variant) newClassNames.push('btn-' + variant);
if(size) newClassNames.push('btn-' + size);
if(active) newClassNames.push('active');
if(disabled) newClassNames.push('disabled');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ variant, size, active, disabled, classNames ]);
return <Flex center classNames={ getClassNames } { ...rest } />;
}

View File

@ -0,0 +1,22 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from './Base';
export interface ButtonGroupProps extends BaseProps<HTMLDivElement>
{
}
export const ButtonGroup: FC<ButtonGroupProps> = props =>
{
const { classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'btn-group' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return <Base classNames={ getClassNames } { ...rest } />;
}

36
src/common/Column.tsx Normal file
View File

@ -0,0 +1,36 @@
import { FC, useMemo } from 'react';
import { Flex, FlexProps } from './Flex';
import { useGridContext } from './GridContext';
import { ColumnSizesType } from './types';
export interface ColumnProps extends FlexProps
{
size?: ColumnSizesType;
column?: boolean;
}
export const Column: FC<ColumnProps> = props =>
{
const { size = 0, column = true, gap = 2, classNames = [], ...rest } = props;
const { isCssGrid = false } = useGridContext();
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [];
if(size)
{
let colClassName = `col-${ size }`;
if(isCssGrid) colClassName = `g-${ colClassName }`;
newClassNames.push(colClassName);
}
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ size, isCssGrid, classNames ]);
return <Flex classNames={ getClassNames } column={ column } gap={ gap } { ...rest } />;
}

54
src/common/Flex.tsx Normal file
View File

@ -0,0 +1,54 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from './Base';
import { AlignItemType, AlignSelfType, JustifyContentType, SpacingType } from './types';
export interface FlexProps extends BaseProps<HTMLDivElement>
{
inline?: boolean;
column?: boolean;
reverse?: boolean;
gap?: SpacingType;
center?: boolean;
alignSelf?: AlignSelfType;
alignItems?: AlignItemType;
justifyContent?: JustifyContentType;
}
export const Flex: FC<FlexProps> = props =>
{
const { inline = false, column = undefined, reverse = false, gap = null, center = false, alignSelf = null, alignItems = null, justifyContent = null, classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [];
if(inline) newClassNames.push('d-inline-flex');
else newClassNames.push('d-flex');
if(column)
{
if(reverse) newClassNames.push('flex-column-reverse');
else newClassNames.push('flex-column');
}
else
{
if(reverse) newClassNames.push('flex-row-reverse');
}
if(gap) newClassNames.push('gap-' + gap);
if(alignSelf) newClassNames.push('align-self-' + alignSelf);
if(alignItems) newClassNames.push('align-items-' + alignItems);
if(justifyContent) newClassNames.push('justify-content-' + justifyContent);
if(!alignItems && !justifyContent && center) newClassNames.push('align-items-center', 'justify-content-center');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ inline, column, reverse, gap, center, alignSelf, alignItems, justifyContent, classNames ]);
return <Base classNames={ getClassNames } { ...rest } />;
}

22
src/common/FormGroup.tsx Normal file
View File

@ -0,0 +1,22 @@
import { FC, useMemo } from 'react';
import { Flex, FlexProps } from './Flex';
export interface FormGroupProps extends FlexProps
{
}
export const FormGroup: FC<FormGroupProps> = props =>
{
const { classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'form-group' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return <Flex classNames={ getClassNames } { ...rest } />;
}

64
src/common/Grid.tsx Normal file
View File

@ -0,0 +1,64 @@
import { FC, useMemo } from 'react';
import { CSSProperties } from 'styled-components';
import { Base, BaseProps } from './Base';
import { GridContextProvider } from './GridContext';
import { AlignItemType, AlignSelfType, JustifyContentType, SpacingType } from './types';
export interface GridProps extends BaseProps<HTMLDivElement>
{
inline?: boolean;
gap?: SpacingType;
maxContent?: boolean;
columnCount?: number;
center?: boolean;
alignSelf?: AlignSelfType;
alignItems?: AlignItemType;
justifyContent?: JustifyContentType;
}
export const Grid: FC<GridProps> = props =>
{
const { inline = false, gap = 2, maxContent = false, columnCount = 0, center = false, alignSelf = null, alignItems = null, justifyContent = null, fullHeight = true, classNames = [], style = {}, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [];
if(inline) newClassNames.push('inline-grid');
else newClassNames.push('grid');
if(gap) newClassNames.push('gap-' + gap);
else if(gap === 0) newClassNames.push('gap-0');
if(maxContent) newClassNames.push('flex-basis-max-content');
if(alignSelf) newClassNames.push('align-self-' + alignSelf);
if(alignItems) newClassNames.push('align-items-' + alignItems);
if(justifyContent) newClassNames.push('justify-content-' + justifyContent);
if(!alignItems && !justifyContent && center) newClassNames.push('align-items-center', 'justify-content-center');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ inline, gap, maxContent, alignSelf, alignItems, justifyContent, center, classNames ]);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
if(columnCount) newStyle['--bs-columns'] = columnCount.toString();
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ columnCount, style ]);
return (
<GridContextProvider value={ { isCssGrid: true } }>
<Base fullHeight={ fullHeight } classNames={ getClassNames } style={ getStyle } { ...rest } />
</GridContextProvider>
);
}

View File

@ -0,0 +1,17 @@
import { createContext, FC, ProviderProps, useContext } from 'react';
export interface IGridContext
{
isCssGrid: boolean;
}
const GridContext = createContext<IGridContext>({
isCssGrid: false
});
export const GridContextProvider: FC<ProviderProps<IGridContext>> = props =>
{
return <GridContext.Provider value={ props.value }>{ props.children }</GridContext.Provider>
}
export const useGridContext = () => useContext(GridContext);

63
src/common/Text.tsx Normal file
View File

@ -0,0 +1,63 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from './Base';
import { ColorVariantType, FontSizeType, FontWeightType, TextAlignType } from './types';
export interface TextProps extends BaseProps<HTMLDivElement>
{
variant?: ColorVariantType;
fontWeight?: FontWeightType;
fontSize?: FontSizeType;
align?: TextAlignType;
bold?: boolean;
underline?: boolean;
italics?: boolean;
truncate?: boolean;
center?: boolean;
textEnd?: boolean;
small?: boolean;
wrap?: boolean;
noWrap?: boolean;
textBreak?: boolean;
}
export const Text: FC<TextProps> = props =>
{
const { variant = 'black', fontWeight = null, fontSize = 0, align = null, bold = false, underline = false, italics = false, truncate = false, center = false, textEnd = false, small = false, wrap = false, noWrap = false, textBreak = false, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'd-inline' ];
if(variant) newClassNames.push('text-' + variant);
if(bold) newClassNames.push('fw-bold');
if(fontWeight) newClassNames.push('fw-' + fontWeight);
if(fontSize) newClassNames.push('fs-' + fontSize);
if(align) newClassNames.push('text-' + align);
if(underline) newClassNames.push('text-decoration-underline');
if(italics) newClassNames.push('fst-italic');
if(truncate) newClassNames.push('text-truncate');
if(center) newClassNames.push('text-center');
if(textEnd) newClassNames.push('text-end');
if(small) newClassNames.push('small');
if(wrap) newClassNames.push('text-wrap');
if(noWrap) newClassNames.push('text-nowrap');
if(textBreak) newClassNames.push('text-break');
return newClassNames;
}, [ variant, fontWeight, fontSize, align, bold, underline, italics, truncate, center, textEnd, small, wrap, noWrap, textBreak ]);
return <Base classNames={ getClassNames } { ...rest } />;
}

43
src/common/index.scss Normal file
View File

@ -0,0 +1,43 @@
.layout-grid-item {
height: var(--nitro-grid-column-min-height, 45px);
background-position: center;
background-repeat: no-repeat;
background-color: $grid-bg-color;
&.active {
border-color: $grid-active-border-color !important;
background-color: $grid-active-bg-color;
}
&.disabled {
cursor: not-allowed;
img {
opacity: .5;
filter: grayscale(1);
}
}
&.unseen {
background-color: rgba($success, 0.4);
}
.avatar-image {
background-position-y: -35px;
}
&.has-highlight {
&:after {
content: "";
z-index: 2;
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background-color: $white;
opacity: 0.1;
}
}
}

13
src/common/index.ts Normal file
View File

@ -0,0 +1,13 @@
export * from './AutoGrid';
export * from './Base';
export * from './Button';
export * from './ButtonGroup';
export * from './Column';
export * from './Flex';
export * from './FormGroup';
export * from './Grid';
export * from './GridContext';
export * from './layout';
export * from './Text';
export * from './types';
export * from './utils';

View File

@ -0,0 +1,75 @@
import { FC, useMemo } from 'react';
import { ItemCountView } from '../../views/shared/item-count/ItemCountView';
import { LimitedEditionStyledNumberView } from '../../views/shared/limited-edition/LimitedEditionStyledNumberView';
import { Base } from '../Base';
import { Column, ColumnProps } from '../Column';
export interface LayoutGridItemProps extends ColumnProps
{
itemImage?: string;
itemColor?: string;
itemActive?: boolean;
itemCount?: number;
itemCountMinimum?: number;
itemUniqueSoldout?: boolean;
itemUniqueNumber?: number;
itemUnseen?: boolean;
itemHighlight?: boolean;
disabled?: boolean;
}
export const LayoutGridItem: FC<LayoutGridItemProps> = props =>
{
const { itemImage = undefined, itemColor = undefined, itemActive = false, itemCount = 1, itemCountMinimum = 1, itemUniqueSoldout = false, itemUniqueNumber = -2, itemUnseen = false, itemHighlight = false, disabled = false, center = true, column = true, style = {}, classNames = [], position = 'relative', overflow = 'hidden', children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'layout-grid-item', 'border', 'border-2', 'border-muted', 'rounded' ];
if(itemActive) newClassNames.push('active');
if(itemUniqueSoldout || (itemUniqueNumber > 0)) newClassNames.push('unique-item');
if(itemUniqueSoldout) newClassNames.push('sold-out');
if(itemUnseen) newClassNames.push('unseen');
if(itemHighlight) newClassNames.push('has-highlight');
if(disabled) newClassNames.push('disabled')
if(itemImage === null) newClassNames.push('icon', 'loading-icon');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ itemActive, itemUniqueSoldout, itemUniqueNumber, itemUnseen, itemHighlight, disabled, itemImage, classNames ]);
const getStyle = useMemo(() =>
{
let newStyle = { ...style };
if(itemImage) newStyle.backgroundImage = `url(${ itemImage })`;
if(itemColor) newStyle.backgroundColor = itemColor;
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ style, itemImage, itemColor ]);
return (
<Column center={ center } pointer position={ position } overflow={ overflow } column={ column } classNames={ getClassNames } style={ getStyle } { ...rest }>
{ (itemCount > itemCountMinimum) &&
<ItemCountView count={ itemCount } /> }
{ (itemUniqueNumber > 0) &&
<>
<Base fit className="unique-bg-override" style={ { backgroundImage: `url(${ itemImage })` } } />
<div className="position-absolute bottom-0 unique-item-counter">
<LimitedEditionStyledNumberView value={ itemUniqueNumber } />
</div>
</> }
{ children }
</Column>
);
}

View File

@ -0,0 +1,23 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from '../Base';
export interface LayoutImageProps extends BaseProps<HTMLDivElement>
{
imageUrl?: string;
}
export const LayoutImage: FC<LayoutImageProps> = props =>
{
const { imageUrl = null, fit = true, style = null, ...rest } = props;
const getStyle = useMemo(() =>
{
const newStyle = { ...style };
if(imageUrl) newStyle.background = `url(${ imageUrl }) center no-repeat`;
return newStyle;
}, [ style, imageUrl ]);
return <Base fit={ fit } style={ getStyle } { ...rest } />;
}

View File

@ -0,0 +1,2 @@
export * from './LayoutGridItem';
export * from './LayoutImage';

View File

@ -0,0 +1 @@
export type AlignItemType = 'start' | 'end' | 'center' | 'baseline' | 'stretch';

View File

@ -0,0 +1 @@
export type AlignSelfType = 'start' | 'end' | 'center' | 'baseline' | 'stretch';

View File

@ -0,0 +1 @@
export type ButtonSizeType = 'lg' | 'sm';

View File

@ -0,0 +1 @@
export type ColorVariantType = 'primary' | 'success' | 'danger' | 'secondary' | 'link' | 'black' | 'white' | 'dark' | 'warning' | 'muted';

View File

@ -0,0 +1 @@
export type ColumnSizesType = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;

View File

@ -0,0 +1 @@
export type FloatType = 'start' | 'end' | 'none';

View File

@ -0,0 +1 @@
export type FontSizeType = 1 | 2 | 3 | 4 | 5 | 6;

View File

@ -0,0 +1 @@
export type FontWeightType = 'bold' | 'bolder' | 'normal' | 'light' | 'lighter';

View File

@ -0,0 +1 @@
export type JustifyContentType = 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly';

View File

@ -0,0 +1 @@
export type OverflowType = 'hidden' | 'auto' | 'unset';

View File

@ -0,0 +1 @@
export type PositionType = 'static' | 'relative' | 'fixed' | 'absolute' | 'sticky';

View File

@ -0,0 +1 @@
export type SpacingType = 0 | 1 | 2 | 3 | 4 | 5;

View File

@ -0,0 +1 @@
export type TextAlignType = 'start' | 'center' | 'end';

13
src/common/types/index.ts Normal file
View File

@ -0,0 +1,13 @@
export * from './AlignItemType';
export * from './AlignSelfType';
export * from './ButtonSizeType';
export * from './ColorVariantType';
export * from './ColumnSizesType';
export * from './FloatType';
export * from './FontSizeType';
export * from './FontWeightType';
export * from './JustifyContentType';
export * from './OverflowType';
export * from './PositionType';
export * from './SpacingType';
export * from './TextAlignType';

View File

@ -0,0 +1,14 @@
import { NitroToolbarAnimateIconEvent } from '@nitrots/nitro-renderer';
import { GetRoomEngine } from '../../api';
export const CreateTransitionToIcon = (image: HTMLImageElement, fromElement: HTMLElement, icon: string) =>
{
const bounds = fromElement.getBoundingClientRect();
const x = (bounds.x + (bounds.width / 2));
const y = (bounds.y + (bounds.height / 2));
const event = new NitroToolbarAnimateIconEvent(image, x, y);
event.iconName = icon;
GetRoomEngine().events.dispatchEvent(event);
}

View File

@ -0,0 +1 @@
export * from './CreateTransitionToIcon';

View File

@ -3,19 +3,6 @@
height: $achievement-height; height: $achievement-height;
} }
.nitro-achievements-category-grid {
--nitro-grid-column-min-width: 80px !important;
.grid-item {
height: 80px;
max-height: 80px;
.achievement-score {
top: 50px;
}
}
}
.nitro-achievements-back-arrow { .nitro-achievements-back-arrow {
background: url('../../assets/images/achievements/back-arrow.png') no-repeat center; background: url('../../assets/images/achievements/back-arrow.png') no-repeat center;
width: 33px; width: 33px;
@ -23,6 +10,6 @@
} }
.nitro-achievements-badge-image { .nitro-achievements-badge-image {
width: 80px; width: 80px !important;
height: 80px; height: 80px !important;
} }

View File

@ -1,18 +1,21 @@
import { AchievementData, AchievementEvent, AchievementsEvent, AchievementsScoreEvent, RequestAchievementsMessageComposer } from '@nitrots/nitro-renderer'; import { AchievementData, AchievementEvent, AchievementsEvent, AchievementsScoreEvent, RequestAchievementsMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { LocalizeText } from '../../api'; import { GetConfiguration, LocalizeText } from '../../api';
import { Base } from '../../common/Base';
import { Column } from '../../common/Column';
import { Flex } from '../../common/Flex';
import { Text } from '../../common/Text';
import { AchievementsUIEvent, AchievementsUIUnseenCountEvent } from '../../events/achievements'; import { AchievementsUIEvent, AchievementsUIUnseenCountEvent } from '../../events/achievements';
import { BatchUpdates, CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../hooks'; import { BatchUpdates, CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../hooks';
import { useUiEvent } from '../../hooks/events'; import { useUiEvent } from '../../hooks/events';
import { NitroCardContentView, NitroCardHeaderView, NitroCardSubHeaderView, NitroCardView, NitroLayoutFlexColumn, NitroLayoutGrid, NitroLayoutGridColumn } from '../../layout'; import { NitroCardContentView, NitroCardHeaderView, NitroCardSubHeaderView, NitroCardView } from '../../layout';
import { NitroLayoutBase } from '../../layout/base'; import { NitroLayoutBase } from '../../layout/base';
import { AchievementsViewProps } from './AchievementsView.types';
import { AchievementCategory } from './common/AchievementCategory'; import { AchievementCategory } from './common/AchievementCategory';
import { AchievementUtilities } from './common/AchievementUtilities'; import { AchievementUtilities } from './common/AchievementUtilities';
import { AchievementsCategoryListView } from './views/category-list/AchievementsCategoryListView'; import { AchievementsCategoryListView } from './views/category-list/AchievementsCategoryListView';
import { AchievementCategoryView } from './views/category/AchievementCategoryView'; import { AchievementCategoryView } from './views/category/AchievementCategoryView';
export const AchievementsView: FC<AchievementsViewProps> = props => export const AchievementsView: FC<{}> = props =>
{ {
const [ isVisible, setIsVisible ] = useState(false); const [ isVisible, setIsVisible ] = useState(false);
const [ isInitalized, setIsInitalized ] = useState(false); const [ isInitalized, setIsInitalized ] = useState(false);
@ -170,6 +173,15 @@ export const AchievementsView: FC<AchievementsViewProps> = props =>
return achievementCategories.find(existing => (existing.code === selectedCategoryCode)); return achievementCategories.find(existing => (existing.code === selectedCategoryCode));
}, [ achievementCategories, selectedCategoryCode ]); }, [ achievementCategories, selectedCategoryCode ]);
const getCategoryIcon = useMemo(() =>
{
if(!getSelectedCategory) return null;
const imageUrl = GetConfiguration<string>('achievements.images.url');
return imageUrl.replace('%image%', `achicon_${ getSelectedCategory.code }`);
}, [ getSelectedCategory ]);
const setAchievementSeen = useCallback((code: string, achievementId: number) => const setAchievementSeen = useCallback((code: string, achievementId: number) =>
{ {
const newCategories = [ ...achievementCategories ]; const newCategories = [ ...achievementCategories ];
@ -207,38 +219,27 @@ export const AchievementsView: FC<AchievementsViewProps> = props =>
<NitroCardView uniqueKey="achievements" className="nitro-achievements" simple={ true }> <NitroCardView uniqueKey="achievements" className="nitro-achievements" simple={ true }>
<NitroCardHeaderView headerText={ LocalizeText('inventory.achievements') } onCloseClick={ event => setIsVisible(false) } /> <NitroCardHeaderView headerText={ LocalizeText('inventory.achievements') } onCloseClick={ event => setIsVisible(false) } />
{ getSelectedCategory && { getSelectedCategory &&
<NitroCardSubHeaderView className="justify-content-center align-items-center cursor-pointer" gap={ 3 }> <NitroCardSubHeaderView position="relative" className="justify-content-center align-items-center cursor-pointer" gap={ 3 }>
<NitroLayoutBase onClick={ event => setSelectedCategoryCode(null) } className="nitro-achievements-back-arrow" /> <NitroLayoutBase onClick={ event => setSelectedCategoryCode(null) } className="nitro-achievements-back-arrow" />
<NitroLayoutFlexColumn className="flex-grow-1"> <Column grow gap={ 0 }>
<NitroLayoutBase className="fs-4 text-black fw-bold"> <Text fontSize={ 4 } fontWeight="bold" className="text-small">{ LocalizeText(`quests.${ getSelectedCategory.code }.name`) }</Text>
{ LocalizeText(`quests.${ getSelectedCategory.code }.name`) } <Text>{ LocalizeText('achievements.details.categoryprogress', [ 'progress', 'limit' ], [ getSelectedCategory.getProgress().toString(), getSelectedCategory.getMaxProgress().toString() ]) }</Text>
</NitroLayoutBase> </Column>
<NitroLayoutBase className="text-black">
{ LocalizeText('achievements.details.categoryprogress', [ 'progress', 'limit' ], [ getSelectedCategory.getProgress().toString(), getSelectedCategory.getMaxProgress().toString() ]) }
</NitroLayoutBase>
</NitroLayoutFlexColumn>
</NitroCardSubHeaderView> } </NitroCardSubHeaderView> }
<NitroCardContentView> <NitroCardContentView>
<NitroLayoutGrid> { !getSelectedCategory &&
<NitroLayoutGridColumn size={ 12 }> <>
{ !getSelectedCategory && <AchievementsCategoryListView categories={ achievementCategories } selectedCategoryCode={ selectedCategoryCode } setSelectedCategoryCode={ setSelectedCategoryCode } />
<> <Column grow justifyContent="end">
<AchievementsCategoryListView categories={ achievementCategories } selectedCategoryCode={ selectedCategoryCode } setSelectedCategoryCode={ setSelectedCategoryCode } /> <Base className="progress" position="relative">
<NitroLayoutFlexColumn className="flex-grow-1 justify-content-end" gap={ 2 }> <Flex fit center position="absolute" className="text-black">{ LocalizeText('achievements.categories.totalprogress', [ 'progress', 'limit' ], [ getProgress.toString(), getMaxProgress.toString() ]) }</Flex>
<NitroLayoutBase className="progress"> <Base className="progress-bar bg-success" style={ { width: (scaledProgressPercent + '%') }} />
<NitroLayoutBase className="progress-bar" style={ { width: (scaledProgressPercent + '%') }}> </Base>
{ LocalizeText('achievements.categories.totalprogress', [ 'progress', 'limit' ], [ getProgress.toString(), getMaxProgress.toString() ]) } <Text className="bg-muted rounded p-1" center>{ LocalizeText('achievements.categories.score', [ 'score' ], [ achievementScore.toString() ]) }</Text>
</NitroLayoutBase> </Column>
</NitroLayoutBase> </> }
<NitroLayoutBase className="bg-muted text-black text-center rounded"> { getSelectedCategory &&
{ LocalizeText('achievements.categories.score', [ 'score' ], [ achievementScore.toString() ]) } <AchievementCategoryView category={ getSelectedCategory } setAchievementSeen={ setAchievementSeen } /> }
</NitroLayoutBase>
</NitroLayoutFlexColumn>
</> }
{ getSelectedCategory &&
<AchievementCategoryView category={ getSelectedCategory } setAchievementSeen={ setAchievementSeen } /> }
</NitroLayoutGridColumn>
</NitroLayoutGrid>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
); );

View File

@ -1,7 +1,14 @@
import { AchievementData } from '@nitrots/nitro-renderer';
import { FC } from 'react'; import { FC } from 'react';
import { BadgeImageView } from '../../../shared/badge-image/BadgeImageView'; import { BaseProps } from '../../../../common/Base';
import { BadgeImageView } from '../../../../views/shared/badge-image/BadgeImageView';
import { AchievementUtilities } from '../../common/AchievementUtilities'; import { AchievementUtilities } from '../../common/AchievementUtilities';
import { AchievementBadgeViewProps } from './AchievementBadgeView.types';
export interface AchievementBadgeViewProps extends BaseProps<HTMLDivElement>
{
achievement: AchievementData;
scale?: number;
}
export const AchievementBadgeView: FC<AchievementBadgeViewProps> = props => export const AchievementBadgeView: FC<AchievementBadgeViewProps> = props =>
{ {

View File

@ -0,0 +1,66 @@
import { AchievementData } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { LocalizeBadgeDescription, LocalizeBadgeName, LocalizeText } from '../../../../api';
import { Base } from '../../../../common/Base';
import { Column } from '../../../../common/Column';
import { Flex } from '../../../../common/Flex';
import { Text } from '../../../../common/Text';
import { CurrencyIcon } from '../../../../views/shared/currency-icon/CurrencyIcon';
import { AchievementUtilities } from '../../common/AchievementUtilities';
import { GetAchievementLevel } from '../../common/GetAchievementLevel';
import { GetScaledProgressPercent } from '../../common/GetScaledProgressPercent';
import { AchievementBadgeView } from '../achievement-badge/AchievementBadgeView';
export interface AchievementDetailsViewProps
{
achievement: AchievementData;
}
export const AchievementDetailsView: FC<AchievementDetailsViewProps> = props =>
{
const { achievement = null } = props;
if(!achievement) return null;
const achievementLevel = GetAchievementLevel(achievement);
const scaledProgressPercent = GetScaledProgressPercent(achievement);
return (
<Flex shrink className="bg-muted rounded p-2 text-black" gap={ 2 } overflow="hidden">
<Column center>
<AchievementBadgeView className="nitro-achievements-badge-image" achievement={ achievement } scale={ 2 } />
<Text fontWeight="bold">
{ LocalizeText('achievements.details.level', [ 'level', 'limit' ], [ achievementLevel.toString(), achievement.levelCount.toString() ]) }
</Text>
</Column>
<Column fullWidth justifyContent="center" overflow="hidden">
<Column gap={ 1 }>
<Text fontWeight="bold" truncate>
{ LocalizeBadgeName(AchievementUtilities.getBadgeCode(achievement)) }
</Text>
<Text truncate>
{ LocalizeBadgeDescription(AchievementUtilities.getBadgeCode(achievement)) }
</Text>
</Column>
{ ((achievement.levelRewardPoints > 0) || (achievement.scoreLimit > 0)) &&
<Column gap={ 1 }>
{ (achievement.levelRewardPoints > 0) &&
<Flex alignItems="center" gap={ 1 }>
<Text truncate className="small">
{ LocalizeText('achievements.details.reward') }
</Text>
<Flex center className="fw-bold small" gap={ 1 }>
{ achievement.levelRewardPoints }
<CurrencyIcon type={ achievement.levelRewardPointType } />
</Flex>
</Flex> }
{ (achievement.scoreLimit > 0) &&
<Base className="progress" position="relative">
<Flex fit center position="absolute" className="text-black"> { LocalizeText('achievements.details.progress', [ 'progress', 'limit' ], [ (achievement.currentPoints + achievement.scoreAtStartOfLevel).toString(), (achievement.scoreLimit + achievement.scoreAtStartOfLevel).toString() ]) }</Flex>
<Base className="progress-bar" style={ { width: (scaledProgressPercent + '%') }} />
</Base> }
</Column> }
</Column>
</Flex>
)
}

View File

@ -0,0 +1,23 @@
import { AchievementData } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { LayoutGridItem, LayoutGridItemProps } from '../../../../common/layout/LayoutGridItem';
import { AchievementBadgeView } from '../achievement-badge/AchievementBadgeView';
export interface AchievementListItemViewProps extends LayoutGridItemProps
{
achievement: AchievementData;
}
export const AchievementListItemView: FC<AchievementListItemViewProps> = props =>
{
const { achievement = null, children = null, ...rest } = props;
if(!achievement) return null;
return (
<LayoutGridItem itemCount={ achievement.unseen } itemCountMinimum={ 0 } { ...rest }>
<AchievementBadgeView achievement={ achievement } />
{ children }
</LayoutGridItem>
);
}

View File

@ -0,0 +1,26 @@
import { AchievementData } from '@nitrots/nitro-renderer';
import { Dispatch, FC, SetStateAction } from 'react';
import { AutoGrid } from '../../../../common/AutoGrid';
import { AchievementListItemView } from './AchievementListItemView';
export interface AchievementListViewProps
{
achievements: AchievementData[];
selectedAchievementId: number;
setSelectedAchievementId: Dispatch<SetStateAction<number>>;
}
export const AchievementListView: FC<AchievementListViewProps> = props =>
{
const { achievements = null, selectedAchievementId = 0, setSelectedAchievementId = null, children = null } = props;
return (
<AutoGrid columnCount={ 6 } columnMinWidth={ 50 } columnMinHeight={ 50 }>
{ achievements && (achievements.length > 0) && achievements.map((achievement, index) =>
{
return <AchievementListItemView key={ index } achievement={ achievement } itemActive={ (selectedAchievementId === achievement.achievementId) } onClick={ event => setSelectedAchievementId(achievement.achievementId) } />;
}) }
{ children }
</AutoGrid>
);
}

View File

@ -0,0 +1,44 @@
import { FC, useCallback, useMemo } from 'react';
import { GetConfiguration, LocalizeText } from '../../../../api';
import { LayoutGridItem, LayoutGridItemProps } from '../../../../common/layout/LayoutGridItem';
import { LayoutImage } from '../../../../common/layout/LayoutImage';
import { Text } from '../../../../common/Text';
import { AchievementCategory } from '../../common/AchievementCategory';
export interface AchievementCategoryListItemViewProps extends LayoutGridItemProps
{
category: AchievementCategory;
}
export const AchievementsCategoryListItemView: FC<AchievementCategoryListItemViewProps> = props =>
{
const { category = null, ...rest } = props;
const progress = category.getProgress();
const maxProgress = category.getMaxProgress();
const getCategoryImage = useMemo(() =>
{
const imageUrl = GetConfiguration<string>('achievements.images.url');
return imageUrl.replace('%image%', `achcategory_${ category.code }_${ ((progress > 0) ? 'active' : 'inactive') }`);
}, [ category, progress ]);
const getTotalUnseen = useCallback(() =>
{
let unseen = 0;
for(const achievement of category.achievements) unseen += achievement.unseen;
return unseen;
}, [ category ]);
return (
<LayoutGridItem itemCount={ getTotalUnseen() } itemCountMinimum={ 0 } gap={ 1 } { ...rest }>
<Text fullWidth center className="small pt-1">{ LocalizeText(`quests.${ category.code }.name`) }</Text>
<LayoutImage position="relative" imageUrl={ getCategoryImage }>
<Text fullWidth center position="absolute" variant="white" style={ { fontSize: 12, bottom: 9 } }>{ progress } / { maxProgress }</Text>
</LayoutImage>
</LayoutGridItem>
);
}

View File

@ -0,0 +1,23 @@
import { Dispatch, FC, SetStateAction } from 'react';
import { AutoGrid } from '../../../../common/AutoGrid';
import { AchievementCategory } from '../../common/AchievementCategory';
import { AchievementsCategoryListItemView } from './AchievementsCategoryListItemView';
export interface AchievementsCategoryListViewProps
{
categories: AchievementCategory[];
selectedCategoryCode: string;
setSelectedCategoryCode: Dispatch<SetStateAction<string>>;
}
export const AchievementsCategoryListView: FC<AchievementsCategoryListViewProps> = props =>
{
const { categories = null, selectedCategoryCode = null, setSelectedCategoryCode = null, children = null } = props;
return (
<AutoGrid columnCount={ 3 } columnMinWidth={ 90 } columnMinHeight={ 100 }>
{ categories && (categories.length > 0) && categories.map((category, index) => <AchievementsCategoryListItemView key={ index } category={ category } itemActive={ (selectedCategoryCode === category.code) } onClick={ event => setSelectedCategoryCode(category.code) } /> ) }
{ children }
</AutoGrid>
);
};

View File

@ -1,8 +1,14 @@
import { FC, useEffect, useMemo, useState } from 'react'; import { FC, useEffect, useMemo, useState } from 'react';
import { NitroLayoutFlexColumn } from '../../../../layout'; import { Column } from '../../../../common/Column';
import { AchievementCategory } from '../../common/AchievementCategory';
import { AchievementDetailsView } from '../achievement-details/AchievementDetailsView'; import { AchievementDetailsView } from '../achievement-details/AchievementDetailsView';
import { AchievementListView } from '../achievement-list/AchievementListView'; import { AchievementListView } from '../achievement-list/AchievementListView';
import { AchievementCategoryViewProps } from './AchievementCategoryView.types';
export class AchievementCategoryViewProps
{
category: AchievementCategory;
setAchievementSeen: (code: string, achievementId: number) => void;
}
export const AchievementCategoryView: FC<AchievementCategoryViewProps> = props => export const AchievementCategoryView: FC<AchievementCategoryViewProps> = props =>
{ {
@ -42,10 +48,10 @@ export const AchievementCategoryView: FC<AchievementCategoryViewProps> = props =
if(!category) return null; if(!category) return null;
return ( return (
<NitroLayoutFlexColumn className="justify-content-between h-100" gap={ 2 }> <Column fullHeight justifyContent="between">
<AchievementListView achievements={ category.achievements } selectedAchievementId={ selectedAchievementId } setSelectedAchievementId={ setSelectedAchievementId } /> <AchievementListView achievements={ category.achievements } selectedAchievementId={ selectedAchievementId } setSelectedAchievementId={ setSelectedAchievementId } />
{ getSelectedAchievement && { getSelectedAchievement &&
<AchievementDetailsView achievement={ getSelectedAchievement } /> } <AchievementDetailsView achievement={ getSelectedAchievement } /> }
</NitroLayoutFlexColumn> </Column>
); );
} }

View File

@ -192,7 +192,7 @@
} }
&.spotlight { &.spotlight-icon {
width: 130px; width: 130px;
height: 305px; height: 305px;
background-position: -5px -5px; background-position: -5px -5px;
@ -212,6 +212,51 @@
} }
} }
.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 { .nitro-avatar-editor {
width: $avatar-editor-width; width: $avatar-editor-width;
height: $avatar-editor-height; height: $avatar-editor-height;
@ -251,9 +296,11 @@
z-index: 4; z-index: 4;
} }
.spotlight { .avatar-spotlight {
position: absolute; position: absolute;
top: -10px; top: -10px;
left: 0;
right: 0;
margin: 0 auto; margin: 0 auto;
opacity: 0.3; opacity: 0.3;
pointer-events: none; pointer-events: none;
@ -287,71 +334,3 @@
} }
} }
} }
.nitro-wardrobe-grid {
--nitro-grid-column-min-width: 80px !important;
.grid-item {
height: 140px;
max-height: 140px;
background-color: $ghost;
&:after {
position: absolute;
content: '';
top: 75%;
bottom: 0;
left: 0;
right: 0;
border-radius: 50%;
background-color: $gray-chateau;
box-shadow: 0 0 8px 2px rgba($white,.6);
transform: scale(2);
}
.avatar-image {
position: absolute;
bottom: 0;
background-position-y: -23px !important;
z-index: 4;
}
.figure-button-container {
background-color: $gray-chateau;
z-index: 3;
}
}
.grid-item-container {
height: 142px !important;
max-height: 142px !important;
.grid-item {
background-color: $ghost;
.avatar-image {
position: absolute;
bottom: 0;
background-position-y: -23px !important;
z-index: 3;
}
.figure-button-container {
background-color: $gray-chateau;
z-index: 2;
}
&:after {
position: absolute;
content: '';
height: 50%;
bottom: 0;
left: 0;
right: 0;
background-color: $gray-chateau;
box-shadow: 0 0 8px 2px rgba($white,.6);
z-index: 1;
}
}
}
}

View File

@ -1,11 +1,15 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetWardrobeMessageComposer, IAvatarFigureContainer, UserFigureComposer, UserWardrobePageEvent } from '@nitrots/nitro-renderer'; import { AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetWardrobeMessageComposer, IAvatarFigureContainer, UserFigureComposer, UserWardrobePageEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react'; import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { GetAvatarRenderManager, GetClubMemberLevel, GetSessionDataManager, LocalizeText } from '../../api'; import { GetAvatarRenderManager, GetClubMemberLevel, GetConfiguration, GetSessionDataManager, LocalizeText } from '../../api';
import { Button } from '../../common/Button';
import { ButtonGroup } from '../../common/ButtonGroup';
import { Column } from '../../common/Column';
import { Grid } from '../../common/Grid';
import { AvatarEditorEvent } from '../../events/avatar-editor'; import { AvatarEditorEvent } from '../../events/avatar-editor';
import { CreateMessageHook, SendMessageHook } from '../../hooks'; import { CreateMessageHook, SendMessageHook } from '../../hooks';
import { useUiEvent } from '../../hooks/events/ui/ui-event'; import { useUiEvent } from '../../hooks/events/ui/ui-event';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, NitroLayoutGrid, NitroLayoutGridColumn } from '../../layout'; import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../layout';
import { AvatarEditorViewProps } from './AvatarEditorView.types';
import { AvatarEditorAction } from './common/AvatarEditorAction'; import { AvatarEditorAction } from './common/AvatarEditorAction';
import { AvatarEditorUtilities } from './common/AvatarEditorUtilities'; import { AvatarEditorUtilities } from './common/AvatarEditorUtilities';
import { BodyModel } from './common/BodyModel'; import { BodyModel } from './common/BodyModel';
@ -15,20 +19,14 @@ import { HeadModel } from './common/HeadModel';
import { IAvatarEditorCategoryModel } from './common/IAvatarEditorCategoryModel'; import { IAvatarEditorCategoryModel } from './common/IAvatarEditorCategoryModel';
import { LegModel } from './common/LegModel'; import { LegModel } from './common/LegModel';
import { TorsoModel } from './common/TorsoModel'; import { TorsoModel } from './common/TorsoModel';
import { AvatarEditorFigureActionsView } from './views/figure-actions/AvatarEditorFigureActionsView';
import { AvatarEditorFigurePreviewView } from './views/figure-preview/AvatarEditorFigurePreviewView'; import { AvatarEditorFigurePreviewView } from './views/figure-preview/AvatarEditorFigurePreviewView';
import { AvatarEditorModelView } from './views/model/AvatarEditorModelView'; import { AvatarEditorModelView } from './views/model/AvatarEditorModelView';
import { AvatarEditorWardrobeView } from './views/wardrobe/AvatarEditorWardrobeView'; import { AvatarEditorWardrobeView } from './views/wardrobe/AvatarEditorWardrobeView';
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_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'; const DEFAULT_FEMALE_FIGURE: string = 'hr-515-33.hd-600-1.ch-635-70.lg-716-66-62.sh-735-68';
const MAX_SAVED_FIGURES: number = 10;
const ACTION_CLEAR = 'action_clear';
const ACTION_RESET = 'action_reset';
const ACTION_RANDOMIZE = 'action_randomize';
const ACTION_SAVE = 'action_save';
export const AvatarEditorView: FC<AvatarEditorViewProps> = props => export const AvatarEditorView: FC<{}> = props =>
{ {
const [ isVisible, setIsVisible ] = useState(false); const [ isVisible, setIsVisible ] = useState(false);
const [ figures, setFigures ] = useState<Map<string, FigureData>>(null); const [ figures, setFigures ] = useState<Map<string, FigureData>>(null);
@ -37,13 +35,15 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
const [ activeCategory, setActiveCategory ] = useState<IAvatarEditorCategoryModel>(null); const [ activeCategory, setActiveCategory ] = useState<IAvatarEditorCategoryModel>(null);
const [ figureSetIds, setFigureSetIds ] = useState<number[]>([]); const [ figureSetIds, setFigureSetIds ] = useState<number[]>([]);
const [ boundFurnitureNames, setBoundFurnitureNames ] = useState<string[]>([]); const [ boundFurnitureNames, setBoundFurnitureNames ] = useState<string[]>([]);
const [ savedFigures, setSavedFigures ] = useState<[ IAvatarFigureContainer, string ][]>(new Array(MAX_SAVED_FIGURES)); const [ savedFigures, setSavedFigures ] = useState<[ IAvatarFigureContainer, string ][]>([]);
const [ isWardrobeVisible, setIsWardrobeVisible ] = useState(false); const [ isWardrobeVisible, setIsWardrobeVisible ] = useState(false);
const [ lastFigure, setLastFigure ] = useState<string>(null); const [ lastFigure, setLastFigure ] = useState<string>(null);
const [ lastGender, setLastGender ] = useState<string>(null); const [ lastGender, setLastGender ] = useState<string>(null);
const [ needsReset, setNeedsReset ] = useState(false); const [ needsReset, setNeedsReset ] = useState(false);
const [ isInitalized, setIsInitalized ] = useState(false); const [ isInitalized, setIsInitalized ] = useState(false);
const maxWardrobeSlots = useMemo(() => GetConfiguration<number>('avatar.wardrobe.max.slots', 10), []);
const onAvatarEditorEvent = useCallback((event: AvatarEditorEvent) => const onAvatarEditorEvent = useCallback((event: AvatarEditorEvent) =>
{ {
switch(event.type) switch(event.type)
@ -89,7 +89,7 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
let i = 0; let i = 0;
while(i < MAX_SAVED_FIGURES) while(i < maxWardrobeSlots)
{ {
savedFigures.push([ null, null ]); savedFigures.push([ null, null ]);
@ -104,7 +104,7 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
} }
setSavedFigures(savedFigures) setSavedFigures(savedFigures)
}, []); }, [ maxWardrobeSlots ]);
CreateMessageHook(UserWardrobePageEvent, onUserWardrobePageEvent); CreateMessageHook(UserWardrobePageEvent, onUserWardrobePageEvent);
@ -195,6 +195,11 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
setFigureData(figures.get(gender)); setFigureData(figures.get(gender));
}, [ figures ]); }, [ figures ]);
useEffect(() =>
{
setSavedFigures(new Array(maxWardrobeSlots));
}, [ maxWardrobeSlots ]);
useEffect(() => useEffect(() =>
{ {
if(!isWardrobeVisible) return; if(!isWardrobeVisible) return;
@ -285,18 +290,33 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
</NitroCardTabsItemView> </NitroCardTabsItemView>
</NitroCardTabsView> </NitroCardTabsView>
<NitroCardContentView> <NitroCardContentView>
<NitroLayoutGrid> <Grid>
<NitroLayoutGridColumn size={ 9 }> <Column size={ 9 } overflow="hidden">
{ (activeCategory && !isWardrobeVisible) && { (activeCategory && !isWardrobeVisible) &&
<AvatarEditorModelView model={ activeCategory } gender={ figureData.gender } setGender={ setGender } /> } <AvatarEditorModelView model={ activeCategory } gender={ figureData.gender } setGender={ setGender } /> }
{ isWardrobeVisible && { isWardrobeVisible &&
<AvatarEditorWardrobeView figureData={ figureData } savedFigures={ savedFigures } setSavedFigures={ setSavedFigures } loadAvatarInEditor={ loadAvatarInEditor } /> } <AvatarEditorWardrobeView figureData={ figureData } savedFigures={ savedFigures } setSavedFigures={ setSavedFigures } loadAvatarInEditor={ loadAvatarInEditor } /> }
</NitroLayoutGridColumn> </Column>
<NitroLayoutGridColumn overflow="hidden" size={ 3 }> <Column size={ 3 } overflow="hidden">
<AvatarEditorFigurePreviewView figureData={ figureData } /> <AvatarEditorFigurePreviewView figureData={ figureData } />
<AvatarEditorFigureActionsView processAction={ processAction } /> <Column grow gap={ 1 }>
</NitroLayoutGridColumn> <ButtonGroup>
</NitroLayoutGrid> <Button variant="secondary" size="sm" onClick={ event => processAction(AvatarEditorAction.ACTION_RESET) }>
<FontAwesomeIcon icon="undo" />
</Button>
<Button variant="secondary" size="sm" onClick={ event => processAction(AvatarEditorAction.ACTION_CLEAR) }>
<FontAwesomeIcon icon="trash" />
</Button>
<Button variant="secondary" size="sm" onClick={ event => processAction(AvatarEditorAction.ACTION_RANDOMIZE) }>
<FontAwesomeIcon icon="dice" />
</Button>
</ButtonGroup>
<Button className="w-100" variant="success" size="sm" onClick={ event => processAction(AvatarEditorAction.ACTION_SAVE) }>
{ LocalizeText('avatareditor.save') }
</Button>
</Column>
</Column>
</Grid>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
); );

Some files were not shown because too many files have changed in this diff Show More