Merge branch 'main' into pr/wired-freeze-furni-movement-ui-clean-20260318

This commit is contained in:
DuckieTM
2026-03-19 10:03:32 +01:00
committed by GitHub
65 changed files with 4866 additions and 526 deletions
+96
View File
@@ -0,0 +1,96 @@
{
"moderation.modtools.title": "Mod Tools",
"moderation.modtools.roomtool": "Room Tool",
"moderation.modtools.roomchatlogs": "Chatlog Tool",
"moderation.modtools.userinfo": "User Info",
"moderation.modtools.tickets": "Tickets",
"moderation.roomtool.info.title": "Room Info",
"moderation.roomtool.roomowner.title": "Owner:",
"moderation.roomtool.usersinroom.title": "Users in room:",
"moderation.roomtool.ownerinroom.title": "Owner here:",
"moderation.roomtool.true.title": "Yes",
"moderation.roomtool.false.title": "No",
"moderation.roomtool.button.visit.title": "Visit Room",
"moderation.roomtool.kickall.title": "Kick everyone out",
"moderation.roomtool.closeroom.title": "Enable the doorbell",
"moderation.roomtool.inappropiatename.title": "Change room name",
"moderation.roomtool.presets.title": "Type a mandatory message...",
"moderation.roomtool.button.caution.title": "Send Caution",
"moderation.roomtool.button.message.title": "Send Alert",
"moderation.tickets.title": "Tickets",
"moderation.tickets.open": "Open Issues",
"moderation.tickets.my": "My Issues",
"moderation.tickets.picked": "Picked Issues",
"moderation.tickets.col.type": "Type",
"moderation.tickets.col.roomPlayer": "Room/Player",
"moderation.tickets.col.opened": "Opened",
"moderation.tickets.col.picker": "Picker",
"moderation.tickets.pick": "Pick Issue",
"moderation.tickets.handle": "Handle",
"moderation.tickets.release": "Release",
"moderation.issue.resolving": "Resolving issue %id%",
"moderation.issue.info": "Issue Information",
"moderation.issue.source": "Source",
"moderation.issue.category": "Category",
"moderation.issue.description": "Description",
"moderation.issue.caller": "Caller",
"moderation.issue.reported": "Reported User",
"moderation.issue.chatlog": "Chatlog",
"moderation.issue.close.useless": "Close as useless",
"moderation.issue.close.abusive": "Close as abusive",
"moderation.issue.close.resolved": "Close as resolved",
"moderation.issue.release": "Release",
"moderation.chatlog.issue": "Issue Chatlog",
"moderation.chatlog.room": "Room Chatlog",
"moderation.chatlog.col.time": "Time",
"moderation.chatlog.col.user": "User",
"moderation.chatlog.col.message": "Message",
"moderation.chatlog.visit": "Visit",
"moderation.chatlog.roomtools": "Room Tools",
"moderation.chatlog.user": "User Chatlog: %username%",
"moderation.userinfo.roomchat": "Room Chat",
"moderation.userinfo.sendmessage": "Send Message",
"moderation.userinfo.roomvisits": "Room Visits",
"moderation.userinfo.modaction": "Mod Action",
"moderation.sendmessage.title": "Send Message",
"moderation.sendmessage.to": "Message To: %username%",
"moderation.sendmessage.send": "Send message",
"moderation.sendmessage.error.empty": "Please write a message to user.",
"moderation.error": "Error",
"moderation.roomvisits.title": "User Visits",
"moderation.roomvisits.col.time": "Time",
"moderation.roomvisits.col.roomname": "Room name",
"moderation.roomvisits.col.visit": "Visit",
"moderation.roomvisits.visitroom": "Visit Room",
"moderation.modaction.title": "Mod Action: %username%",
"moderation.modaction.cfhtopic": "CFH Topic",
"moderation.modaction.sanctiontype": "Sanction Type",
"moderation.modaction.message.hint": "Optional message type, overrides default",
"moderation.modaction.defaultsanction": "Default Sanction",
"moderation.modaction.sanction": "Sanction",
"moderation.modaction.alert": "Alert",
"moderation.modaction.mute1h": "Mute 1h",
"moderation.modaction.ban18h": "Ban 18h",
"moderation.modaction.ban7days": "Ban 7 days",
"moderation.modaction.ban30days.step1": "Ban 30 days (step 1)",
"moderation.modaction.ban30days.step2": "Ban 30 days (step 2)",
"moderation.modaction.ban100years": "Ban 100 years",
"moderation.modaction.banavataronly100years": "Ban avatar-only 100 years",
"moderation.modaction.kick": "Kick",
"moderation.modaction.locktrade1week": "Lock trade 1 week",
"moderation.modaction.locktradepermanent": "Lock trade permanent",
"moderation.modaction.message": "Message",
"moderation.modaction.error.notopic": "You must select a CFH topic",
"moderation.modaction.error.notopicorsanction": "You must select a CFH topic and Sanction",
"moderation.modaction.error.nopermission": "You do not have permission to do this",
"moderation.modaction.error.nosanction": "You must select a sanction",
"moderation.modaction.error.emptymessage": "Please write a message to user"
}
+1
View File
@@ -21,6 +21,7 @@
"framer-motion": "^11.2.12",
"react": "^19.2.4",
"react-bootstrap": "^2.10.10",
"react-colorful": "^5.6.1",
"react-dom": "^19.2.4",
"react-icons": "^5.5.0",
"react-slider": "^2.0.6",
File diff suppressed because it is too large Load Diff
+9 -7
View File
@@ -1,6 +1,6 @@
import { GetAssetManager, GetAvatarRenderManager, GetCommunication, GetConfiguration, GetLocalizationManager, GetRoomEngine, GetRoomSessionManager, GetSessionDataManager, GetSoundManager, GetStage, GetTexturePool, GetTicker, HabboWebTools, LegacyExternalInterface, LoadGameUrlEvent, NitroLogger, NitroVersion, PrepareRenderer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { GetUIVersion } from './api';
import { GetUIVersion, UiSettingsProvider } from './api';
import { Base } from './common';
import { LoadingView } from './components/loading/LoadingView';
import { MainView } from './components/MainView';
@@ -89,11 +89,13 @@ export const App: FC<{}> = props =>
}, []);
return (
<Base fit overflow="hidden" className={ !(window.devicePixelRatio % 1) && 'image-rendering-pixelated' }>
{ !isReady &&
<LoadingView /> }
{ isReady && <MainView /> }
<Base id="draggable-windows-container" />
</Base>
<UiSettingsProvider>
<Base fit overflow="hidden" className={ !(window.devicePixelRatio % 1) && 'image-rendering-pixelated' }>
{ !isReady &&
<LoadingView /> }
{ isReady && <MainView /> }
<Base id="draggable-windows-container" />
</Base>
</UiSettingsProvider>
);
};
+21
View File
@@ -0,0 +1,21 @@
export interface IUiSettings
{
colorMode: 'color' | 'image' | 'default';
headerColor: string;
headerImageUrl: string;
headerAlpha: number;
}
export const DEFAULT_UI_SETTINGS: IUiSettings = {
colorMode: 'default',
headerColor: '#1E7295',
headerImageUrl: '',
headerAlpha: 100
};
export const PRESET_COLORS: string[] = [
'#000000', '#444444', '#888888', '#CCCCCC', '#660000', '#CC3333', '#FF6666', '#CC6600',
'#FF3333', '#FF6633', '#FF9933', '#FFCC00', '#FFFF00', '#66FF00', '#00CC00', '#009900',
'#00FFCC', '#33CCFF', '#3366FF', '#0000CC', '#6633CC', '#9933FF', '#CC33FF', '#FF66CC',
'#FF99CC', '#1E7295', '#185D79', '#2DABC2', '#2B91A7', '#283F5D'
];
+164
View File
@@ -0,0 +1,164 @@
import { createContext, FC, PropsWithChildren, useCallback, useContext, useEffect, useState } from 'react';
import { DEFAULT_UI_SETTINGS, IUiSettings } from './IUiSettings';
const STORAGE_KEY = 'nitro.ui.settings';
interface IUiSettingsContext
{
settings: IUiSettings;
isCustomActive: boolean;
updateSettings: (partial: Partial<IUiSettings>) => void;
resetSettings: () => void;
getHeaderStyle: () => React.CSSProperties;
getTabsStyle: () => React.CSSProperties;
getAccentColor: () => string;
}
const UiSettingsContext = createContext<IUiSettingsContext>({
settings: DEFAULT_UI_SETTINGS,
isCustomActive: false,
updateSettings: () => {},
resetSettings: () => {},
getHeaderStyle: () => ({}),
getTabsStyle: () => ({}),
getAccentColor: () => DEFAULT_UI_SETTINGS.headerColor
});
const darkenColor = (hex: string, amount: number): string =>
{
const num = parseInt(hex.replace('#', ''), 16);
const r = Math.max(0, ((num >> 16) & 0xFF) - amount);
const g = Math.max(0, ((num >> 8) & 0xFF) - amount);
const b = Math.max(0, (num & 0xFF) - amount);
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
};
const loadSettings = (): IUiSettings =>
{
try
{
const stored = localStorage.getItem(STORAGE_KEY);
if(stored) return { ...DEFAULT_UI_SETTINGS, ...JSON.parse(stored) };
}
catch(e) {}
return { ...DEFAULT_UI_SETTINGS };
};
const saveSettings = (settings: IUiSettings): void =>
{
try
{
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
}
catch(e) {}
};
export const UiSettingsProvider: FC<PropsWithChildren> = ({ children }) =>
{
const [ settings, setSettings ] = useState<IUiSettings>(loadSettings);
const updateSettings = useCallback((partial: Partial<IUiSettings>) =>
{
setSettings(prev =>
{
const updated = { ...prev, ...partial };
saveSettings(updated);
return updated;
});
}, []);
const resetSettings = useCallback(() =>
{
setSettings({ ...DEFAULT_UI_SETTINGS });
saveSettings(DEFAULT_UI_SETTINGS);
}, []);
const getHeaderStyle = useCallback((): React.CSSProperties =>
{
if(settings.colorMode === 'color')
{
return { backgroundColor: settings.headerColor };
}
if(settings.colorMode === 'image' && settings.headerImageUrl)
{
return {
backgroundImage: `url(${ settings.headerImageUrl })`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'repeat'
};
}
return {};
}, [ settings ]);
const getTabsStyle = useCallback((): React.CSSProperties =>
{
if(settings.colorMode === 'color')
{
return { backgroundColor: darkenColor(settings.headerColor, 30) };
}
if(settings.colorMode === 'image' && settings.headerImageUrl)
{
return {
backgroundImage: `url(${ settings.headerImageUrl })`,
backgroundSize: 'cover',
backgroundPosition: 'center bottom',
backgroundRepeat: 'repeat'
};
}
return {};
}, [ settings ]);
const getAccentColor = useCallback((): string =>
{
if(settings.colorMode === 'color') return settings.headerColor;
return DEFAULT_UI_SETTINGS.headerColor;
}, [ settings ]);
const isCustomActive = settings.colorMode !== 'default';
const ALL_CSS_VARS = [
'--ui-accent-color', '--ui-accent-dark',
'--ui-ctx-bg', '--ui-ctx-header-bg', '--ui-ctx-item-bg1', '--ui-ctx-item-bg2',
'--ui-btn-primary-bg', '--ui-btn-primary-border',
'--ui-dark-bg', '--ui-dark-border'
];
useEffect(() =>
{
const root = document.documentElement;
if(settings.colorMode === 'color')
{
const c = settings.headerColor;
root.style.setProperty('--ui-accent-color', c);
root.style.setProperty('--ui-accent-dark', darkenColor(c, 30));
root.style.setProperty('--ui-ctx-bg', darkenColor(c, 50));
root.style.setProperty('--ui-ctx-header-bg', darkenColor(c, 20));
root.style.setProperty('--ui-ctx-item-bg1', darkenColor(c, 60));
root.style.setProperty('--ui-ctx-item-bg2', darkenColor(c, 70));
root.style.setProperty('--ui-btn-primary-bg', c);
root.style.setProperty('--ui-btn-primary-border', darkenColor(c, 20));
root.style.setProperty('--ui-dark-bg', darkenColor(c, 55));
root.style.setProperty('--ui-dark-border', darkenColor(c, 60));
}
else
{
ALL_CSS_VARS.forEach(v => root.style.removeProperty(v));
}
}, [ settings ]);
return (
<UiSettingsContext.Provider value={ { settings, isCustomActive, updateSettings, resetSettings, getHeaderStyle, getTabsStyle, getAccentColor } }>
{ children }
</UiSettingsContext.Provider>
);
};
export const useUiSettings = () => useContext(UiSettingsContext);
+2
View File
@@ -0,0 +1,2 @@
export * from './IUiSettings';
export * from './UiSettingsContext';
+6
View File
@@ -15,4 +15,10 @@ export class WiredTriggerLayout
public static BOT_REACHED_STUFF: number = 13;
public static BOT_REACHED_AVATAR: number = 14;
public static RECEIVE_SIGNAL: number = 15;
public static AVATAR_LEAVES_ROOM: number = 16;
public static EXECUTE_PERIODICALLY_SHORT: number = 17;
public static CLICK_FURNI: number = 18;
public static CLICK_TILE: number = 19;
public static CLICK_USER: number = 20;
public static USER_PERFORMS_ACTION: number = 21;
}
+29 -11
View File
@@ -12,20 +12,16 @@ export interface ButtonProps extends FlexProps
export const Button: FC<ButtonProps> = props =>
{
const { variant = 'primary', size = 'sm', active = false, disabled = false, classNames = [], ...rest } = props;
const { variant = 'primary', size = 'sm', active = false, disabled = false, classNames = [], style = {}, ...rest } = props;
const getClassNames = useMemo(() =>
{
// fucked up method i know (i dont have a clue what im doing because im a ninja)
const newClassNames: string[] = [ 'pointer-events-auto inline-block font-normal leading-normal text-[#fff] text-center no-underline align-middle cursor-pointer select-none border border-[solid] border-transparent px-[.75rem] py-[.375rem] text-[.9rem] rounded-[.25rem] [transition:color_.15s_ease-in-out,background-color_.15s_ease-in-out,border-color_.15s_ease-in-out,box-shadow_.15s_ease-in-out]' ];
if(variant)
{
if(variant == 'primary')
newClassNames.push('text-white bg-[#1e7295] border-[#1e7295] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#1a617f] hover:border-[#185b77]');
newClassNames.push('text-white [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white');
if(variant == 'success')
newClassNames.push('text-white bg-[#00800b] border-[#00800b] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#006d09] hover:border-[#006609]');
@@ -43,11 +39,10 @@ export const Button: FC<ButtonProps> = props =>
newClassNames.push('text-white bg-[#185d79] border-[#185d79] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#144f67] hover:border-[#134a61]');
if(variant == 'dark')
newClassNames.push('text-white bg-dark [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#18181bfb] hover:border-[#161619fb]');
if(variant == 'gray')
newClassNames.push('text-white bg-[#1e7295] border-[#1e7295] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#1a617f] hover:border-[#185b77]');
newClassNames.push('text-white [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white');
if(variant == 'gray')
newClassNames.push('text-white [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white');
}
if(size)
@@ -67,5 +62,28 @@ export const Button: FC<ButtonProps> = props =>
return newClassNames;
}, [ variant, size, active, disabled, classNames ]);
return <Flex center classNames={ getClassNames } { ...rest } />;
const getStyle = useMemo(() =>
{
if(variant === 'primary' || variant === 'gray')
{
return {
backgroundColor: 'var(--ui-btn-primary-bg, #1e7295)',
borderColor: 'var(--ui-btn-primary-border, #1e7295)',
...style
};
}
if(variant === 'dark')
{
return {
backgroundColor: 'var(--ui-dark-bg, rgba(28, 28, 32, .98))',
borderColor: 'var(--ui-dark-border, rgba(28, 28, 32, .98))',
...style
};
}
return style;
}, [ variant, style ]);
return <Flex center classNames={ getClassNames } style={ getStyle } { ...rest } />;
};
+18 -4
View File
@@ -11,11 +11,25 @@ export interface SliderProps extends ReactSliderProps
export const Slider: FC<SliderProps> = props =>
{
const { disabledButton, max, min, value, onChange, ...rest } = props;
const { disabledButton, max, min, step, value, onChange, ...rest } = props;
const currentValue = Array.isArray(value) ? value[0] : ((typeof value === 'number') ? value : 0);
const minimum = (typeof min === 'number') ? min : 0;
const maximum = (typeof max === 'number') ? max : 0;
const buttonStep = ((typeof step === 'number') && (step > 0)) ? step : 1;
const roundToStep = (nextValue: number) =>
{
if(typeof buttonStep !== 'number') return nextValue;
const decimalStep = buttonStep.toString();
const precision = decimalStep.includes('.') ? (decimalStep.length - decimalStep.indexOf('.') - 1) : 0;
return parseFloat(nextValue.toFixed(precision));
};
return <Flex fullWidth gap={ 1 }>
{ !disabledButton && <Button disabled={ min >= value } onClick={ () => onChange(min < value ? value - 1 : min, 0) }><FaAngleLeft /></Button> }
<ReactSlider className={ 'nitro-slider' } max={ max } min={ min } value={ value } onChange={ onChange } { ...rest } />
{ !disabledButton && <Button disabled={ max <= value } onClick={ () => onChange(max > value ? value + 1 : max, 0) }><FaAngleRight /></Button> }
{ !disabledButton && <Button disabled={ minimum >= currentValue } onClick={ () => onChange(roundToStep(minimum < currentValue ? currentValue - buttonStep : minimum), 0) }><FaAngleLeft /></Button> }
<ReactSlider className={ 'nitro-slider' } max={ max } min={ min } step={ step } value={ value } onChange={ onChange } { ...rest } />
{ !disabledButton && <Button disabled={ maximum <= currentValue } onClick={ () => onChange(roundToStep(maximum > currentValue ? currentValue + buttonStep : maximum), 0) }><FaAngleRight /></Button> }
</Flex>;
}
+7 -3
View File
@@ -1,5 +1,6 @@
import { FC, MouseEvent } from 'react';
import { FaFlag } from 'react-icons/fa';
import { useUiSettings } from '../../api';
import { Base, Column, ColumnProps, Flex } from '..';
interface NitroCardHeaderViewProps extends ColumnProps
@@ -16,8 +17,7 @@ interface NitroCardHeaderViewProps extends ColumnProps
export const NitroCardHeaderView: FC<NitroCardHeaderViewProps> = props =>
{
const { headerText = null, isGalleryPhoto = false, noCloseButton = false, isInfoToHabboPages = false, onReportPhoto = null, onClickInfoHabboPages = null, onCloseClick = null, justifyContent = 'center', alignItems = 'center', classNames = [], children = null, ...rest } = props;
const { isCustomActive, getHeaderStyle } = useUiSettings();
const onMouseDown = (event: MouseEvent<HTMLDivElement>) =>
{
@@ -25,8 +25,12 @@ export const NitroCardHeaderView: FC<NitroCardHeaderViewProps> = props =>
event.nativeEvent.stopImmediatePropagation();
};
const headerClassName = isCustomActive
? 'relative flex items-center justify-center flex-col drag-handler min-h-card-header max-h-card-header'
: 'relative flex items-center justify-center flex-col drag-handler min-h-card-header max-h-card-header bg-card-header';
return (
<Column center className={ 'relative flex items-center justify-center flex-col drag-handler min-h-card-header max-h-card-header bg-card-header' } { ...rest }>
<Column center className={ headerClassName } style={ getHeaderStyle() } { ...rest }>
<Flex center fullWidth>
<span className="text-xl text-white drop-shadow-lg">{ headerText }</span>
{ isGalleryPhoto &&
+9 -3
View File
@@ -1,21 +1,27 @@
import { FC, useMemo } from 'react';
import { useUiSettings } from '../../../api';
import { Flex, FlexProps } from '../..';
export const NitroCardTabsView: FC<FlexProps> = props =>
{
const { justifyContent = 'center', gap = 1, classNames = [], children = null, ...rest } = props;
const { isCustomActive, getTabsStyle } = useUiSettings();
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'justify-center gap-0.5 flex bg-card-tabs min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-px' ];
const base = isCustomActive
? 'justify-center gap-0.5 flex min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-px'
: 'justify-center gap-0.5 flex bg-card-tabs min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-px';
const newClassNames: string[] = [ base ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
}, [ classNames, isCustomActive ]);
return (
<Flex classNames={ getClassNames } gap={ gap } justifyContent={ justifyContent } { ...rest }>
<Flex classNames={ getClassNames } gap={ gap } justifyContent={ justifyContent } style={ getTabsStyle() } { ...rest }>
{ children }
</Flex>
);
+6
View File
@@ -9,6 +9,7 @@ import { CampaignView } from './campaign/CampaignView';
import { CatalogView } from './catalog/CatalogView';
import { ChatHistoryView } from './chat-history/ChatHistoryView';
import { FloorplanEditorView } from './floorplan-editor/FloorplanEditorView';
import { FurniEditorView } from './furni-editor/FurniEditorView';
import { FriendsView } from './friends/FriendsView';
import { GameCenterView } from './game-center/GameCenterView';
import { GroupsView } from './groups/GroupsView';
@@ -21,10 +22,12 @@ import { ModToolsView } from './mod-tools/ModToolsView';
import { NavigatorView } from './navigator/NavigatorView';
import { NitrobubbleHiddenView } from './nitrobubblehidden/NitrobubbleHiddenView';
import { NitropediaView } from './nitropedia/NitropediaView';
import { ExternalPluginLoader } from './plugins/ExternalPluginLoader';
import { RightSideView } from './right-side/RightSideView';
import { RoomView } from './room/RoomView';
import { ToolbarView } from './toolbar/ToolbarView';
import { UserProfileView } from './user-profile/UserProfileView';
import { InterfaceSettingsView } from './interface-settings/InterfaceSettingsView';
import { UserSettingsView } from './user-settings/UserSettingsView';
import { WiredView } from './wired/WiredView';
import { YoutubeTvView } from './youtube-tv/YoutubeTvView';
@@ -106,6 +109,7 @@ export const MainView: FC<{}> = props =>
<FriendsView />
<RightSideView />
<UserSettingsView />
<InterfaceSettingsView />
<UserProfileView />
<GroupsView />
<CameraWidgetView />
@@ -116,7 +120,9 @@ export const MainView: FC<{}> = props =>
<CampaignView />
<GameCenterView />
<FloorplanEditorView />
<FurniEditorView />
<YoutubeTvView />
<ExternalPluginLoader />
</>
);
};
@@ -8,13 +8,25 @@ interface IFloorplanEditorContext
setOriginalFloorplanSettings: Dispatch<SetStateAction<IFloorplanSettings>>;
visualizationSettings: IVisualizationSettings;
setVisualizationSettings: Dispatch<SetStateAction<IVisualizationSettings>>;
floorHeight: number;
setFloorHeight: Dispatch<SetStateAction<number>>;
floorAction: number;
setFloorAction: Dispatch<SetStateAction<number>>;
tilemapVersion: number;
areaInfo: { total: number; walkable: number };
}
const FloorplanEditorContext = createContext<IFloorplanEditorContext>({
originalFloorplanSettings: null,
setOriginalFloorplanSettings: null,
visualizationSettings: null,
setVisualizationSettings: null
setVisualizationSettings: null,
floorHeight: 0,
setFloorHeight: null,
floorAction: 3,
setFloorAction: null,
tilemapVersion: 0,
areaInfo: { total: 0, walkable: 0 }
});
export const FloorplanEditorContextProvider: FC<ProviderProps<IFloorplanEditorContext>> = props => <FloorplanEditorContext.Provider { ...props } />;
@@ -1,19 +1,22 @@
import { AddLinkEventTracker, FloorHeightMapEvent, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomVisualizationSettingsEvent, UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { FC, useCallback, useEffect, useState } from 'react';
import { FaCaretLeft, FaCaretRight } from 'react-icons/fa';
import { LocalizeText, SendMessageComposer } from '../../api';
import { Button, ButtonGroup, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common';
import { Button, ButtonGroup, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
import { useMessageEvent, useNitroEvent } from '../../hooks';
import { FloorplanEditorContextProvider } from './FloorplanEditorContext';
import { FloorplanEditor } from '@nitrots/nitro-renderer';
import { IFloorplanSettings } from '@nitrots/nitro-renderer';
import { IVisualizationSettings } from '@nitrots/nitro-renderer';
import { convertNumbersForSaving, convertSettingToNumber } from '@nitrots/nitro-renderer';
import { convertNumbersForSaving, convertSettingToNumber, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer';
import { FloorplanCanvasView } from './views/FloorplanCanvasView';
import { FloorplanImportExportView } from './views/FloorplanImportExportView';
import { FloorplanOptionsView } from './views/FloorplanOptionsView';
import { FloorplanHeightSelector } from './views/FloorplanHeightSelector';
import { FloorplanPreviewView } from './views/FloorplanPreviewView';
type ScrollDirection = 'up' | 'down' | 'left' | 'right';
const MIN_WALL_HEIGHT = 0;
const MAX_WALL_HEIGHT = 16;
export const FloorplanEditorView: FC<{}> = props =>
{
@@ -34,7 +37,65 @@ export const FloorplanEditorView: FC<{}> = props =>
thicknessWall: 1,
thicknessFloor: 1
});
const [ canvasScrollHandler, setCanvasScrollHandler ] = useState<((direction: ScrollDirection) => void) | null>(null);
const [ floorHeight, setFloorHeight ] = useState(0);
const [ floorAction, setFloorAction ] = useState(FloorAction.SET);
const [ tilemapVersion, setTilemapVersion ] = useState(0);
const [ areaInfo, setAreaInfo ] = useState({ total: 0, walkable: 0 });
const calculateArea = useCallback(() =>
{
const tilemap = FloorplanEditor.instance.tilemap;
if(!tilemap || tilemap.length === 0)
{
setAreaInfo({ total: 0, walkable: 0 });
return;
}
let total = 0;
let walkable = 0;
for(let y = 0; y < tilemap.length; y++)
{
if(!tilemap[y]) continue;
for(let x = 0; x < tilemap[y].length; x++)
{
if(!tilemap[y][x] || tilemap[y][x].height === 'x') continue;
total++;
if(!tilemap[y][x].isBlocked) walkable++;
}
}
setAreaInfo({ total, walkable });
}, []);
// sync floorHeight/floorAction changes to the FloorplanEditor instance
useEffect(() =>
{
FloorplanEditor.instance.actionSettings.currentAction = floorAction;
FloorplanEditor.instance.actionSettings.currentHeight = floorHeight.toString(36);
}, [ floorHeight, floorAction ]);
// register onTilemapChange callback
useEffect(() =>
{
if(!isVisible) return;
FloorplanEditor.instance.onTilemapChange = () =>
{
setTilemapVersion(prev => prev + 1);
calculateArea();
};
return () =>
{
FloorplanEditor.instance.onTilemapChange = null;
};
}, [ isVisible, calculateArea ]);
const saveFloorChanges = () =>
{
@@ -47,16 +108,50 @@ export const FloorplanEditorView: FC<{}> = props =>
convertNumbersForSaving(visualizationSettings.thicknessFloor),
(visualizationSettings.wallHeight - 1)
));
}
};
const revertChanges = () =>
{
setVisualizationSettings({ wallHeight: originalFloorplanSettings.wallHeight, thicknessWall: originalFloorplanSettings.thicknessWall, thicknessFloor: originalFloorplanSettings.thicknessFloor, entryPointDir: originalFloorplanSettings.entryPointDir });
FloorplanEditor.instance.doorLocation = { x: originalFloorplanSettings.entryPoint[0], y: originalFloorplanSettings.entryPoint[1] };
FloorplanEditor.instance.setTilemap(originalFloorplanSettings.tilemap, originalFloorplanSettings.reservedTiles);
FloorplanEditor.instance.renderTiles();
}
};
const onWallHeightChange = (value: number) =>
{
if(isNaN(value) || (value <= 0)) value = MIN_WALL_HEIGHT;
if(value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT;
setVisualizationSettings(prevValue =>
{
const newValue = { ...prevValue };
newValue.wallHeight = value;
return newValue;
});
};
const increaseWallHeight = () =>
{
let height = (visualizationSettings.wallHeight + 1);
if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT;
onWallHeightChange(height);
};
const decreaseWallHeight = () =>
{
let height = (visualizationSettings.wallHeight - 1);
if(height <= 0) height = MIN_WALL_HEIGHT;
onWallHeightChange(height);
};
useNitroEvent<RoomEngineEvent>(RoomEngineEvent.DISPOSED, event => setIsVisible(false));
@@ -117,7 +212,7 @@ export const FloorplanEditorView: FC<{}> = props =>
const parts = url.split('/');
if(parts.length < 2) return;
switch(parts[1])
{
case 'show':
@@ -140,17 +235,42 @@ export const FloorplanEditorView: FC<{}> = props =>
}, []);
return (
<FloorplanEditorContextProvider value={ { originalFloorplanSettings: originalFloorplanSettings, setOriginalFloorplanSettings: setOriginalFloorplanSettings, visualizationSettings: visualizationSettings, setVisualizationSettings: setVisualizationSettings } }>
<FloorplanEditorContextProvider value={ {
originalFloorplanSettings,
setOriginalFloorplanSettings,
visualizationSettings,
setVisualizationSettings,
floorHeight,
setFloorHeight,
floorAction,
setFloorAction,
tilemapVersion,
areaInfo
} }>
{ isVisible &&
<NitroCardView uniqueKey="floorpan-editor" className="w-[760px] h-[500px]" theme="primary-slim">
<NitroCardView uniqueKey="floorpan-editor" className="w-[1100px] h-[600px]" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('floor.plan.editor.title') } onCloseClick={ () => setIsVisible(false) } />
<NitroCardContentView overflow="hidden">
<FloorplanOptionsView onCanvasScroll={ direction => canvasScrollHandler && canvasScrollHandler(direction) } />
<FloorplanCanvasView overflow="hidden" setScrollHandler={ setCanvasScrollHandler } />
<NitroCardContentView overflow="hidden" className="flex flex-col">
<FloorplanOptionsView />
<Flex gap={ 2 } className="flex-1 min-h-0">
<FloorplanHeightSelector />
<FloorplanCanvasView overflow="hidden" />
<Column gap={ 2 } className="w-[380px] min-w-[380px]">
<FloorplanPreviewView />
<Flex gap={ 1 } alignItems="center">
<Text bold small>{ LocalizeText('floor.editor.wall.height') }</Text>
<FaCaretLeft className="cursor-pointer fa-icon" onClick={ decreaseWallHeight } />
<input type="number" className="form-control form-control-sm w-[49px]" value={ visualizationSettings.wallHeight } onChange={ event => onWallHeightChange(event.target.valueAsNumber) } />
<FaCaretRight className="cursor-pointer fa-icon" onClick={ increaseWallHeight } />
</Flex>
<Text bold small className="text-center">
Area: { areaInfo.total } ({ areaInfo.walkable } caselle)
</Text>
</Column>
</Flex>
<Flex justifyContent="between">
<Button onClick={ revertChanges }>{ LocalizeText('floor.plan.editor.reload') }</Button>
<Button variant="danger" onClick={ revertChanges }>{ LocalizeText('floor.plan.editor.reload') }</Button>
<ButtonGroup>
<Button disabled={ true }>{ LocalizeText('floor.plan.editor.preview') }</Button>
<Button onClick={ event => setImportExportVisible(true) }>{ LocalizeText('floor.plan.editor.import.export') }</Button>
<Button onClick={ saveFloorChanges }>{ LocalizeText('floor.plan.editor.save') }</Button>
</ButtonGroup>
@@ -161,4 +281,4 @@ export const FloorplanEditorView: FC<{}> = props =>
<FloorplanImportExportView onCloseClick={ () => setImportExportVisible(false) } /> }
</FloorplanEditorContextProvider>
);
}
};
@@ -1,25 +1,25 @@
import { GetOccupiedTilesMessageComposer, GetRoomEntryTileMessageComposer, RoomEntryTileMessageEvent, RoomOccupiedTilesMessageEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useRef, useState } from 'react';
import { FaPlus, FaMinus } from 'react-icons/fa';
import { SendMessageComposer } from '../../../api';
import { Base, Column, ColumnProps } from '../../../common';
import { useMessageEvent } from '../../../hooks';
import { useFloorplanEditorContext } from '../FloorplanEditorContext';
import { FloorplanEditor } from '@nitrots/nitro-renderer';
type ScrollDirection = 'up' | 'down' | 'left' | 'right';
interface FloorplanCanvasViewProps extends ColumnProps
{
setScrollHandler(handler: ((direction: ScrollDirection) => void) | null): void;
}
export const FloorplanCanvasView: FC<FloorplanCanvasViewProps> = props =>
{
const { gap = 1, children = null, setScrollHandler = null, ...rest } = props;
const [ occupiedTilesReceived , setOccupiedTilesReceived ] = useState(false);
const { gap = 1, children = null, ...rest } = props;
const [ occupiedTilesReceived, setOccupiedTilesReceived ] = useState(false);
const [ entryTileReceived, setEntryTileReceived ] = useState(false);
const [ zoomLevel, setZoomLevel ] = useState(1.0);
const { originalFloorplanSettings = null, setOriginalFloorplanSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext();
const elementRef = useRef<HTMLDivElement>(null);
const canvasWrapperRef = useRef<HTMLDivElement>(null);
useMessageEvent<RoomOccupiedTilesMessageEvent>(RoomOccupiedTilesMessageEvent, event =>
{
@@ -37,7 +37,7 @@ export const FloorplanCanvasView: FC<FloorplanCanvasViewProps> = props =>
});
setOccupiedTilesReceived(true);
elementRef.current.scrollTo((FloorplanEditor.instance.renderer.canvas.width / 3), 0);
});
@@ -63,39 +63,16 @@ export const FloorplanCanvasView: FC<FloorplanCanvasViewProps> = props =>
return newValue;
});
FloorplanEditor.instance.doorLocation = { x: parser.x, y: parser.y };
setEntryTileReceived(true);
});
const onClickArrowButton = (scrollDirection: ScrollDirection) =>
{
const element = elementRef.current;
if(!element) return;
switch(scrollDirection)
{
case 'up':
element.scrollBy({ top: -10 });
break;
case 'down':
element.scrollBy({ top: 10 });
break;
case 'left':
element.scrollBy({ left: -10 });
break;
case 'right':
element.scrollBy({ left: 10 });
break;
}
}
const onPointerEvent = (event: PointerEvent) =>
{
event.preventDefault();
switch(event.type)
{
case 'pointerout':
@@ -109,7 +86,10 @@ export const FloorplanCanvasView: FC<FloorplanCanvasViewProps> = props =>
FloorplanEditor.instance.onPointerMove(event);
break;
}
}
};
const zoomIn = () => setZoomLevel(prev => Math.min(prev + 0.25, 2.0));
const zoomOut = () => setZoomLevel(prev => Math.max(prev - 0.25, 0.5));
useEffect(() =>
{
@@ -124,15 +104,15 @@ export const FloorplanCanvasView: FC<FloorplanCanvasViewProps> = props =>
thicknessWall: originalFloorplanSettings.thicknessWall,
thicknessFloor: originalFloorplanSettings.thicknessFloor,
entryPointDir: prevValue.entryPointDir
}
};
});
}
};
}, [ originalFloorplanSettings.thicknessFloor, originalFloorplanSettings.thicknessWall, originalFloorplanSettings.wallHeight, setVisualizationSettings ]);
useEffect(() =>
{
if(!entryTileReceived || !occupiedTilesReceived) return;
FloorplanEditor.instance.renderTiles();
}, [ entryTileReceived, occupiedTilesReceived ]);
@@ -144,45 +124,56 @@ export const FloorplanCanvasView: FC<FloorplanCanvasViewProps> = props =>
const currentElement = elementRef.current;
if(!currentElement) return;
currentElement.appendChild(FloorplanEditor.instance.renderer.canvas);
const wrapper = canvasWrapperRef.current;
if(wrapper) wrapper.appendChild(FloorplanEditor.instance.renderer.canvas);
currentElement.addEventListener('pointerup', onPointerEvent);
currentElement.addEventListener('pointerout', onPointerEvent);
currentElement.addEventListener('pointerdown', onPointerEvent);
currentElement.addEventListener('pointermove', onPointerEvent);
return () =>
return () =>
{
if(currentElement)
{
currentElement.removeEventListener('pointerup', onPointerEvent);
currentElement.removeEventListener('pointerout', onPointerEvent);
currentElement.removeEventListener('pointerdown', onPointerEvent);
currentElement.removeEventListener('pointermove', onPointerEvent);
}
}
};
}, []);
useEffect(() =>
{
if(!setScrollHandler) return;
setScrollHandler(() => onClickArrowButton);
return () => setScrollHandler(null);
}, [ setScrollHandler ]);
return (
<Column gap={ gap } { ...rest }>
<Base overflow="auto" innerRef={ elementRef } />
<Column gap={ gap } { ...rest } className="relative flex-1">
<Base overflow="auto" innerRef={ elementRef } className="flex-1">
<div
ref={ canvasWrapperRef }
style={ {
transform: `scale(${ zoomLevel })`,
transformOrigin: '0 0'
} }
/>
</Base>
<div className="absolute top-2 right-2 flex flex-col gap-1 z-10">
<button
className="w-[28px] h-[28px] flex items-center justify-center rounded bg-[#1e7295] text-white border border-transparent shadow cursor-pointer hover:brightness-110"
onClick={ zoomIn }
title="Zoom in"
>
<FaPlus size={ 10 } />
</button>
<button
className="w-[28px] h-[28px] flex items-center justify-center rounded bg-[#1e7295] text-white border border-transparent shadow cursor-pointer hover:brightness-110"
onClick={ zoomOut }
title="Zoom out"
>
<FaMinus size={ 10 } />
</button>
</div>
{ children }
</Column>
);
}
};
@@ -0,0 +1,54 @@
import { FC } from 'react';
import { COLORMAP, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer';
import { FloorplanEditor } from '@nitrots/nitro-renderer';
import { Column, Text } from '../../../common';
import { useFloorplanEditorContext } from '../FloorplanEditorContext';
const colormap = COLORMAP as Record<string, string>;
export const FloorplanHeightSelector: FC<{}> = () =>
{
const { floorHeight, setFloorHeight, setFloorAction } = useFloorplanEditorContext();
const onSelectHeight = (height: number) =>
{
setFloorHeight(height);
setFloorAction(FloorAction.SET);
FloorplanEditor.instance.actionSettings.currentAction = FloorAction.SET;
FloorplanEditor.instance.actionSettings.currentHeight = height.toString(36);
};
const heights: number[] = [];
for(let i = 26; i >= 0; i--) heights.push(i);
return (
<Column className="h-full w-[30px] min-w-[30px] select-none">
<Text bold small center>{ floorHeight }</Text>
<div className="flex flex-col flex-1 rounded overflow-hidden border-2 border-muted">
{ heights.map(h =>
{
const char = HEIGHT_SCHEME[h + 1];
const color = colormap[char] || '101010';
const isActive = (floorHeight === h);
return (
<div
key={ h }
className="flex-1 cursor-pointer relative flex items-center justify-center"
style={ {
backgroundColor: `#${ color }`,
outline: isActive ? '2px solid #fff' : 'none',
outlineOffset: '-2px',
zIndex: isActive ? 1 : 0
} }
onClick={ () => onSelectHeight(h) }
title={ `${ h }` }
/>
);
}) }
</div>
</Column>
);
};
@@ -1,45 +1,32 @@
import { FC, useState } from 'react';
import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp, FaCaretLeft, FaCaretRight } from 'react-icons/fa';
import { FC } from 'react';
import { LocalizeText } from '../../../api';
import { Button, Column, Flex, LayoutGridItem, Slider, Text } from '../../../common';
import { COLORMAP, FloorAction } from '@nitrots/nitro-renderer';
import { Flex, LayoutGridItem, Text } from '../../../common';
import { FloorAction } from '@nitrots/nitro-renderer';
import { FloorplanEditor } from '@nitrots/nitro-renderer';
import { useFloorplanEditorContext } from '../FloorplanEditorContext';
const MIN_WALL_HEIGHT: number = 0;
const MAX_WALL_HEIGHT: number = 16;
const MIN_FLOOR_HEIGHT: number = 0;
const MAX_FLOOR_HEIGHT: number = 26;
type ScrollDirection = 'up' | 'down' | 'left' | 'right';
interface FloorplanOptionsViewProps
{
onCanvasScroll?(direction: ScrollDirection): void;
}
export const FloorplanOptionsView: FC<FloorplanOptionsViewProps> = props =>
{
const { onCanvasScroll = () => {} } = props;
const { visualizationSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext();
const [ floorAction, setFloorAction ] = useState(FloorAction.SET);
const [ floorHeight, setFloorHeight ] = useState(0);
const [ isSquareSelectMode, setSquareSelectMode ] = useState(false);
const { visualizationSettings = null, setVisualizationSettings = null, floorAction, setFloorAction } = useFloorplanEditorContext();
const isSquareSelectMode = FloorplanEditor.instance.isSquareSelectMode;
const selectAction = (action: number) =>
{
setFloorAction(action);
FloorplanEditor.instance.actionSettings.currentAction = action;
}
};
const toggleSquareSelectMode = () =>
{
const nextValue = FloorplanEditor.instance.toggleSquareSelectMode();
setSquareSelectMode(nextValue);
}
FloorplanEditor.instance.toggleSquareSelectMode();
// force re-render by toggling action to same value
setFloorAction(prev => prev);
};
const changeDoorDirection = () =>
{
@@ -58,18 +45,19 @@ export const FloorplanOptionsView: FC<FloorplanOptionsViewProps> = props =>
return newValue;
});
}
};
const onFloorHeightChange = (value: number) =>
const onWallThicknessChange = (value: number) =>
{
if(isNaN(value) || (value <= 0)) value = 0;
setVisualizationSettings(prevValue =>
{
const newValue = { ...prevValue };
if(value > 26) value = 26;
newValue.thicknessWall = value;
setFloorHeight(value);
FloorplanEditor.instance.actionSettings.currentHeight = value.toString(36);
}
return newValue;
});
};
const onFloorThicknessChange = (value: number) =>
{
@@ -81,157 +69,54 @@ export const FloorplanOptionsView: FC<FloorplanOptionsViewProps> = props =>
return newValue;
});
}
const onWallThicknessChange = (value: number) =>
{
setVisualizationSettings(prevValue =>
{
const newValue = { ...prevValue };
newValue.thicknessWall = value;
return newValue;
});
}
const onWallHeightChange = (value: number) =>
{
if(isNaN(value) || (value <= 0)) value = MIN_WALL_HEIGHT;
if(value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT;
setVisualizationSettings(prevValue =>
{
const newValue = { ...prevValue };
newValue.wallHeight = value;
return newValue;
});
}
const increaseWallHeight = () =>
{
let height = (visualizationSettings.wallHeight + 1);
if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT;
onWallHeightChange(height);
}
const decreaseWallHeight = () =>
{
let height = (visualizationSettings.wallHeight - 1);
if(height <= 0) height = MIN_WALL_HEIGHT;
onWallHeightChange(height);
}
};
return (
<Column>
<Flex gap={ 1 }>
<Column size={ 5 } gap={ 1 }>
<Text bold>{ LocalizeText('floor.plan.editor.draw.mode') }</Text>
<Flex gap={ 3 }>
<Flex gap={ 1 }>
<LayoutGridItem itemActive={ (floorAction === FloorAction.SET) } onClick={ event => selectAction(FloorAction.SET) }>
<i className="nitro-icon icon-set-tile" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.UNSET) } onClick={ event => selectAction(FloorAction.UNSET) }>
<i className="nitro-icon icon-unset-tile" />
</LayoutGridItem>
</Flex>
<Flex gap={ 1 }>
<LayoutGridItem itemActive={ (floorAction === FloorAction.UP) } onClick={ event => selectAction(FloorAction.UP) }>
<i className="nitro-icon icon-increase-height" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.DOWN) } onClick={ event => selectAction(FloorAction.DOWN) }>
<i className="nitro-icon icon-decrease-height" />
</LayoutGridItem>
</Flex>
<LayoutGridItem itemActive={ (floorAction === FloorAction.DOOR) } onClick={ event => selectAction(FloorAction.DOOR) }>
<i className="nitro-icon icon-set-door" />
</LayoutGridItem>
<LayoutGridItem onClick={ event => FloorplanEditor.instance.toggleSelectAll() }>
<i className={ `nitro-icon ${ floorAction === FloorAction.UNSET ? 'icon-set-deselect' : 'icon-set-select' }` } />
</LayoutGridItem>
<LayoutGridItem itemActive={ isSquareSelectMode } onClick={ toggleSquareSelectMode }>
<i className={ `nitro-icon ${ isSquareSelectMode ? 'icon-set-active-squaresselect' : 'icon-set-squaresselect' }` } />
</LayoutGridItem>
</Flex>
</Column>
<Column alignItems="center" size={ 4 }>
<Text bold>{ LocalizeText('floor.plan.editor.enter.direction') }</Text>
<i className={ `nitro-icon icon-door-direction-${ visualizationSettings.entryPointDir } cursor-pointer` } onClick={ changeDoorDirection } />
</Column>
<Column size={ 3 }>
<Text bold>{ LocalizeText('floor.editor.wall.height') }</Text>
<Flex alignItems="center" gap={ 1 }>
<FaCaretLeft className="cursor-pointer fa-icon" onClick={ decreaseWallHeight } />
<input type="number" className="form-control form-control-sm w-[49px]" value={ visualizationSettings.wallHeight } onChange={ event => onWallHeightChange(event.target.valueAsNumber) } />
<FaCaretRight className="cursor-pointer fa-icon" onClick={ increaseWallHeight } />
</Flex>
</Column>
<Column size={ 6 }>
<Text bold>{ LocalizeText('floor.plan.editor.room.options') }</Text>
<Flex className="align-items-center">
<select className="form-control form-control-sm" value={ visualizationSettings.thicknessWall } onChange={ event => onWallThicknessChange(parseInt(event.target.value)) }>
<option value={ 0 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thinnest') }</option>
<option value={ 1 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thin') }</option>
<option value={ 2 }>{ LocalizeText('navigator.roomsettings.wall_thickness.normal') }</option>
<option value={ 3 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thick') }</option>
</select>
<select className="form-control form-control-sm" value={ visualizationSettings.thicknessFloor } onChange={ event => onFloorThicknessChange(parseInt(event.target.value)) }>
<option value={ 0 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thinnest') }</option>
<option value={ 1 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thin') }</option>
<option value={ 2 }>{ LocalizeText('navigator.roomsettings.floor_thickness.normal') }</option>
<option value={ 3 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thick') }</option>
</select>
</Flex>
</Column>
<Flex gap={ 2 } alignItems="center">
<Flex gap={ 1 } alignItems="center">
<Text bold small>{ LocalizeText('floor.plan.editor.draw.mode') }</Text>
<Flex gap={ 1 }>
<LayoutGridItem itemActive={ (floorAction === FloorAction.SET) } onClick={ () => selectAction(FloorAction.SET) }>
<i className="nitro-icon icon-set-tile" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.UNSET) } onClick={ () => selectAction(FloorAction.UNSET) }>
<i className="nitro-icon icon-unset-tile" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.UP) } onClick={ () => selectAction(FloorAction.UP) }>
<i className="nitro-icon icon-increase-height" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.DOWN) } onClick={ () => selectAction(FloorAction.DOWN) }>
<i className="nitro-icon icon-decrease-height" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.DOOR) } onClick={ () => selectAction(FloorAction.DOOR) }>
<i className="nitro-icon icon-set-door" />
</LayoutGridItem>
<LayoutGridItem onClick={ () => FloorplanEditor.instance.toggleSelectAll() }>
<i className={ `nitro-icon ${ floorAction === FloorAction.UNSET ? 'icon-set-deselect' : 'icon-set-select' }` } />
</LayoutGridItem>
<LayoutGridItem itemActive={ isSquareSelectMode } onClick={ toggleSquareSelectMode }>
<i className={ `nitro-icon ${ isSquareSelectMode ? 'icon-set-active-squaresselect' : 'icon-set-squaresselect' }` } />
</LayoutGridItem>
</Flex>
</Flex>
<Flex gap={ 2 } alignItems="center" justifyContent="between">
<Column size={ 6 }>
<Text bold>{ LocalizeText('floor.plan.editor.tile.height') }: { floorHeight }</Text>
<div style={ { width: '100%', maxWidth: 240 } }>
<Slider
min={ MIN_FLOOR_HEIGHT }
max={ MAX_FLOOR_HEIGHT }
step={ 1 }
value={ floorHeight }
onChange={ event => onFloorHeightChange(event) }
renderThumb={ (props, state) =>
{
const { key, style, ...rest } = (props as Record<string, any>);
return <div key={ key } style={ { backgroundColor: `#${ COLORMAP[state.valueNow.toString(33)] }`, ...style } } { ...rest }>{ state.valueNow }</div>;
} } />
</div>
</Column>
<Column gap={ 1 }>
<Flex justifyContent="center">
<Button shrink onClick={ event => onCanvasScroll('up') }>
<FaArrowUp className="fa-icon" />
</Button>
</Flex>
<Flex alignItems="center" justifyContent="center" gap={ 1 }>
<Button shrink onClick={ event => onCanvasScroll('left') }>
<FaArrowLeft className="fa-icon" />
</Button>
<div style={ { width: 28 } } />
<Button shrink onClick={ event => onCanvasScroll('right') }>
<FaArrowRight className="fa-icon" />
</Button>
</Flex>
<Flex justifyContent="center">
<Button shrink onClick={ event => onCanvasScroll('down') }>
<FaArrowDown className="fa-icon" />
</Button>
</Flex>
</Column>
<Flex gap={ 1 } alignItems="center">
<Text bold small>{ LocalizeText('floor.plan.editor.enter.direction') }</Text>
<i className={ `nitro-icon icon-door-direction-${ visualizationSettings.entryPointDir } cursor-pointer` } onClick={ changeDoorDirection } />
</Flex>
</Column>
<Flex gap={ 1 } alignItems="center" className="ml-auto">
<select className="form-control form-control-sm" value={ visualizationSettings.thicknessWall } onChange={ event => onWallThicknessChange(parseInt(event.target.value)) }>
<option value={ 0 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thinnest') }</option>
<option value={ 1 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thin') }</option>
<option value={ 2 }>{ LocalizeText('navigator.roomsettings.wall_thickness.normal') }</option>
<option value={ 3 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thick') }</option>
</select>
<select className="form-control form-control-sm" value={ visualizationSettings.thicknessFloor } onChange={ event => onFloorThicknessChange(parseInt(event.target.value)) }>
<option value={ 0 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thinnest') }</option>
<option value={ 1 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thin') }</option>
<option value={ 2 }>{ LocalizeText('navigator.roomsettings.floor_thickness.normal') }</option>
<option value={ 3 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thick') }</option>
</select>
</Flex>
</Flex>
);
}
};
@@ -0,0 +1,328 @@
import { FC, useEffect, useRef } from 'react';
import { COLORMAP, HEIGHT_SCHEME, FloorplanEditor } from '@nitrots/nitro-renderer';
import { useFloorplanEditorContext } from '../FloorplanEditorContext';
const colormap = COLORMAP as Record<string, string>;
const PREVIEW_TILE_W = 16;
const PREVIEW_TILE_H = 8;
const PREVIEW_BLOCK_H = 5;
const WALL_HEIGHT_PX = 40;
const WALL_COLOR = '#6B7B5E';
const WALL_SIDE_COLOR = '#5A6A4F';
const WALL_TOP_COLOR = '#7D8E6F';
function hexToRgb(hex: string): [number, number, number]
{
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return [ r, g, b ];
}
function rgbToHex(r: number, g: number, b: number): string
{
return `#${ ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) }`;
}
function darken(hex: string, factor: number): string
{
const [ r, g, b ] = hexToRgb(hex);
return rgbToHex(
Math.floor(r * factor),
Math.floor(g * factor),
Math.floor(b * factor)
);
}
function getTilemapBounds(tilemap: any[][]): { minX: number; minY: number; maxX: number; maxY: number }
{
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for(let y = 0; y < tilemap.length; y++)
{
if(!tilemap[y]) continue;
for(let x = 0; x < tilemap[y].length; x++)
{
if(!tilemap[y][x] || tilemap[y][x].height === 'x') continue;
if(x < minX) minX = x;
if(x > maxX) maxX = x;
if(y < minY) minY = y;
if(y > maxY) maxY = y;
}
}
if(minX === Infinity) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
return { minX, minY, maxX, maxY };
}
function renderPreview(canvas: HTMLCanvasElement, wallHeight: number): void
{
const ctx = canvas.getContext('2d');
const tilemap = FloorplanEditor.instance.tilemap;
if(!ctx || !tilemap || tilemap.length === 0)
{
if(ctx)
{
ctx.fillStyle = '#1a1a1a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
return;
}
const bounds = getTilemapBounds(tilemap);
const tilesW = bounds.maxX - bounds.minX + 1;
const tilesH = bounds.maxY - bounds.minY + 1;
// find max height for offset calculation
let maxTileHeight = 0;
for(let y = bounds.minY; y <= bounds.maxY; y++)
{
for(let x = bounds.minX; x <= bounds.maxX; x++)
{
if(!tilemap[y] || !tilemap[y][x] || tilemap[y][x].height === 'x') continue;
const hi = HEIGHT_SCHEME.indexOf(tilemap[y][x].height) - 1;
if(hi > maxTileHeight) maxTileHeight = hi;
}
}
// calculate isometric bounds
const isoW = (tilesW + tilesH) * PREVIEW_TILE_W;
const isoH = (tilesW + tilesH) * PREVIEW_TILE_H + maxTileHeight * PREVIEW_BLOCK_H + WALL_HEIGHT_PX;
// scale to fit canvas
const scaleX = (canvas.width - 20) / isoW;
const scaleY = (canvas.height - 20) / isoH;
const scale = Math.min(scaleX, scaleY, 3);
const offsetX = (canvas.width - isoW * scale) / 2;
const offsetY = (canvas.height - isoH * scale) / 2 + WALL_HEIGHT_PX * scale * 0.5;
ctx.fillStyle = '#1a1a1a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(offsetX, offsetY);
ctx.scale(scale, scale);
const tw = PREVIEW_TILE_W;
const th = PREVIEW_TILE_H;
function isoX(gx: number, gy: number): number
{
return (gx - bounds.minX - gy + bounds.minY) * tw + (tilesH - 1) * tw;
}
function isoY(gx: number, gy: number): number
{
return (gx - bounds.minX + gy - bounds.minY) * th;
}
function hasActiveTile(gx: number, gy: number): boolean
{
return tilemap[gy] && tilemap[gy][gx] && tilemap[gy][gx].height !== 'x';
}
function getTileHeight(gx: number, gy: number): number
{
if(!hasActiveTile(gx, gy)) return 0;
return Math.max(0, HEIGHT_SCHEME.indexOf(tilemap[gy][gx].height) - 1);
}
// draw walls on north and west edges
const wallH = wallHeight > 0 ? wallHeight * PREVIEW_BLOCK_H + WALL_HEIGHT_PX * 0.3 : WALL_HEIGHT_PX * 0.6;
for(let y = bounds.minY; y <= bounds.maxY; y++)
{
for(let x = bounds.minX; x <= bounds.maxX; x++)
{
if(!hasActiveTile(x, y)) continue;
const tileH = getTileHeight(x, y) * PREVIEW_BLOCK_H;
const cx = isoX(x, y);
const cy = isoY(x, y) - tileH;
// west wall (no tile to the left)
if(!hasActiveTile(x - 1, y))
{
ctx.beginPath();
ctx.moveTo(cx, cy + th);
ctx.lineTo(cx, cy + th - wallH);
ctx.lineTo(cx + tw, cy - wallH);
ctx.lineTo(cx + tw, cy);
ctx.closePath();
ctx.fillStyle = WALL_SIDE_COLOR;
ctx.fill();
ctx.strokeStyle = '#4A5A3F';
ctx.lineWidth = 0.5;
ctx.stroke();
}
// north wall (no tile above)
if(!hasActiveTile(x, y - 1))
{
ctx.beginPath();
ctx.moveTo(cx + tw, cy);
ctx.lineTo(cx + tw, cy - wallH);
ctx.lineTo(cx + tw * 2, cy + th - wallH);
ctx.lineTo(cx + tw * 2, cy + th);
ctx.closePath();
ctx.fillStyle = WALL_COLOR;
ctx.fill();
ctx.strokeStyle = '#4A5A3F';
ctx.lineWidth = 0.5;
ctx.stroke();
}
// wall top cap - corner
if(!hasActiveTile(x - 1, y) && !hasActiveTile(x, y - 1))
{
ctx.beginPath();
ctx.moveTo(cx + tw, cy - wallH);
ctx.lineTo(cx + tw + tw * 0.3, cy - wallH - th * 0.3);
ctx.lineTo(cx + tw, cy - wallH - th * 0.6);
ctx.lineTo(cx + tw - tw * 0.3, cy - wallH - th * 0.3);
ctx.closePath();
ctx.fillStyle = WALL_TOP_COLOR;
ctx.fill();
}
}
}
// draw tiles back-to-front
for(let y = bounds.minY; y <= bounds.maxY; y++)
{
for(let x = bounds.minX; x <= bounds.maxX; x++)
{
if(!hasActiveTile(x, y)) continue;
const tile = tilemap[y][x];
const heightIndex = HEIGHT_SCHEME.indexOf(tile.height) - 1;
const tileH = Math.max(0, heightIndex) * PREVIEW_BLOCK_H;
const cx = isoX(x, y);
const cy = isoY(x, y) - tileH;
const heightChar = tile.height;
const baseColor = colormap[heightChar] || 'aaaaaa';
const topColor = `#${ baseColor }`;
const leftColor = darken(baseColor, 0.65);
const rightColor = darken(baseColor, 0.80);
// draw side faces if tile has height
const blockH = Math.max(0, heightIndex) * PREVIEW_BLOCK_H;
// left face (visible when no neighbor to south or neighbor is shorter)
const southH = getTileHeight(x, y + 1);
const leftExpose = hasActiveTile(x, y + 1) ? Math.max(0, heightIndex - southH) * PREVIEW_BLOCK_H : blockH + PREVIEW_BLOCK_H;
if(leftExpose > 0)
{
ctx.beginPath();
ctx.moveTo(cx, cy + th);
ctx.lineTo(cx + tw, cy + th * 2);
ctx.lineTo(cx + tw, cy + th * 2 + leftExpose);
ctx.lineTo(cx, cy + th + leftExpose);
ctx.closePath();
ctx.fillStyle = leftColor;
ctx.fill();
}
// right face
const eastH = getTileHeight(x + 1, y);
const rightExpose = hasActiveTile(x + 1, y) ? Math.max(0, heightIndex - eastH) * PREVIEW_BLOCK_H : blockH + PREVIEW_BLOCK_H;
if(rightExpose > 0)
{
ctx.beginPath();
ctx.moveTo(cx + tw * 2, cy + th);
ctx.lineTo(cx + tw, cy + th * 2);
ctx.lineTo(cx + tw, cy + th * 2 + rightExpose);
ctx.lineTo(cx + tw * 2, cy + th + rightExpose);
ctx.closePath();
ctx.fillStyle = rightColor;
ctx.fill();
}
// top face
ctx.beginPath();
ctx.moveTo(cx + tw, cy);
ctx.lineTo(cx + tw * 2, cy + th);
ctx.lineTo(cx + tw, cy + th * 2);
ctx.lineTo(cx, cy + th);
ctx.closePath();
ctx.fillStyle = topColor;
ctx.fill();
// door indicator
const door = FloorplanEditor.instance.doorLocation;
if(door.x === x && door.y === y)
{
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.fill();
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1;
ctx.stroke();
}
}
}
ctx.restore();
}
export const FloorplanPreviewView: FC<{}> = () =>
{
const { tilemapVersion, visualizationSettings } = useFloorplanEditorContext();
const canvasRef = useRef<HTMLCanvasElement>(null);
const rafRef = useRef<number>(0);
useEffect(() =>
{
if(!canvasRef.current) return;
if(rafRef.current) cancelAnimationFrame(rafRef.current);
rafRef.current = requestAnimationFrame(() =>
{
const canvas = canvasRef.current;
if(!canvas) return;
const parent = canvas.parentElement;
if(parent)
{
canvas.width = parent.clientWidth;
canvas.height = parent.clientHeight;
}
renderPreview(canvas, visualizationSettings?.wallHeight ?? 0);
});
return () =>
{
if(rafRef.current) cancelAnimationFrame(rafRef.current);
};
}, [ tilemapVersion, visualizationSettings?.wallHeight ]);
return (
<div className="flex-1 relative rounded overflow-hidden border-2 border-muted" style={ { minHeight: 200, backgroundColor: '#1a1a1a' } }>
<canvas
ref={ canvasRef }
className="w-full h-full"
/>
</div>
);
};
@@ -0,0 +1,179 @@
import { RgbaColorPicker, RgbaColor } from 'react-colorful';
import { FC, useCallback, useMemo, useState } from 'react';
import { FaUndo, FaTrash, FaPen, FaFillDrip, FaSave } from 'react-icons/fa';
import { PRESET_COLORS, useUiSettings } from '../../api';
import { Flex, Text } from '../../common';
const hexToRgba = (hex: string, a = 1): RgbaColor =>
{
const num = parseInt(hex.replace('#', ''), 16);
return { r: (num >> 16) & 0xFF, g: (num >> 8) & 0xFF, b: num & 0xFF, a };
};
const rgbaToHex = (rgba: RgbaColor): string =>
{
return '#' + ((1 << 24) + (rgba.r << 16) + (rgba.g << 8) + rgba.b).toString(16).slice(1);
};
export const InterfaceColorTabView: FC<{}> = () =>
{
const { settings, updateSettings, resetSettings } = useUiSettings();
const [ color, setColor ] = useState<RgbaColor>(() => hexToRgba(settings.headerColor, settings.headerAlpha / 100));
const hexColor = useMemo(() => rgbaToHex(color), [ color ]);
const alphaPercent = useMemo(() => Math.round((color.a ?? 1) * 100), [ color ]);
const onHexInput = useCallback((value: string) =>
{
const clean = value.replace(/[^0-9a-fA-F]/g, '').slice(0, 6);
if(clean.length === 6)
{
const rgba = hexToRgba('#' + clean, color.a);
setColor(rgba);
}
}, [ color.a ]);
const onRgbInput = useCallback((channel: 'r' | 'g' | 'b', value: number) =>
{
const clamped = Math.max(0, Math.min(255, value || 0));
setColor(prev => ({ ...prev, [channel]: clamped }));
}, []);
const onAlphaInput = useCallback((value: number) =>
{
const clamped = Math.max(0, Math.min(100, value || 0));
setColor(prev => ({ ...prev, a: clamped / 100 }));
}, []);
const onPresetClick = useCallback((presetHex: string) =>
{
setColor(hexToRgba(presetHex, color.a));
}, [ color.a ]);
const onSave = useCallback(() =>
{
updateSettings({
colorMode: 'color',
headerColor: hexColor,
headerAlpha: alphaPercent
});
}, [ updateSettings, hexColor, alphaPercent ]);
const onReset = useCallback(() =>
{
resetSettings();
setColor(hexToRgba('#1E7295', 1));
}, [ resetSettings ]);
const onDelete = useCallback(() =>
{
updateSettings({ colorMode: 'default' });
setColor(hexToRgba('#1E7295', 1));
}, [ updateSettings ]);
return (
<Flex column gap={ 2 } className="items-center p-2">
<div className="w-[280px]">
<RgbaColorPicker color={ color } onChange={ setColor } style={ { width: '100%', height: '180px' } } />
</div>
<Flex gap={ 1 } className="items-center mt-1">
<Flex column className="items-center">
<input
className="form-control form-control-sm text-center w-[70px]"
value={ hexColor.replace('#', '').toUpperCase() }
onChange={ e => onHexInput(e.target.value) }
maxLength={ 6 }
/>
<Text small className="text-black">Hex</Text>
</Flex>
<Flex column className="items-center">
<input
type="number"
className="form-control form-control-sm text-center w-[45px]"
value={ color.r }
onChange={ e => onRgbInput('r', parseInt(e.target.value)) }
min={ 0 } max={ 255 }
/>
<Text small className="text-black">R</Text>
</Flex>
<Flex column className="items-center">
<input
type="number"
className="form-control form-control-sm text-center w-[45px]"
value={ color.g }
onChange={ e => onRgbInput('g', parseInt(e.target.value)) }
min={ 0 } max={ 255 }
/>
<Text small className="text-black">G</Text>
</Flex>
<Flex column className="items-center">
<input
type="number"
className="form-control form-control-sm text-center w-[45px]"
value={ color.b }
onChange={ e => onRgbInput('b', parseInt(e.target.value)) }
min={ 0 } max={ 255 }
/>
<Text small className="text-black">B</Text>
</Flex>
<Flex column className="items-center">
<input
type="number"
className="form-control form-control-sm text-center w-[45px]"
value={ alphaPercent }
onChange={ e => onAlphaInput(parseInt(e.target.value)) }
min={ 0 } max={ 100 }
/>
<Text small className="text-black">A</Text>
</Flex>
</Flex>
<div className="grid grid-cols-10 gap-0.5 mt-1">
{ PRESET_COLORS.map((presetHex, i) => (
<div
key={ i }
className="w-[24px] h-[24px] rounded cursor-pointer border border-black/20 hover:scale-110 transition-transform"
style={ { backgroundColor: presetHex } }
onClick={ () => onPresetClick(presetHex) }
/>
)) }
</div>
<Flex gap={ 1 } className="w-full mt-2">
<button
className="flex-1 flex items-center justify-center gap-1 py-2 rounded cursor-pointer text-white"
style={ { backgroundColor: '#5f9ea0' } }
onClick={ onReset }
>
<FaUndo size={ 14 } />
</button>
<button
className="flex-1 flex items-center justify-center gap-1 py-2 rounded cursor-pointer text-white"
style={ { backgroundColor: '#5f9ea0' } }
onClick={ onDelete }
>
<FaTrash size={ 14 } />
</button>
<button
className="flex-1 flex items-center justify-center gap-1 py-2 rounded cursor-pointer text-white"
style={ { backgroundColor: '#b0b0b0' } }
>
<FaPen size={ 14 } />
</button>
<button
className="flex-1 flex items-center justify-center gap-1 py-2 rounded cursor-pointer text-white"
style={ { backgroundColor: '#5f9ea0' } }
onClick={ onSave }
>
<FaFillDrip size={ 14 } />
</button>
</Flex>
<button
className="w-full py-2 rounded cursor-pointer text-white font-bold flex items-center justify-center gap-2"
style={ { backgroundColor: '#008000' } }
onClick={ onSave }
>
<FaSave size={ 14 } />
Salva colore
</button>
</Flex>
);
};
@@ -0,0 +1,52 @@
import { FC, useCallback, useMemo } from 'react';
import { GetConfigurationValue, useUiSettings } from '../../api';
export const InterfaceImageTabView: FC<{}> = () =>
{
const { settings, updateSettings } = useUiSettings();
const imageCount = useMemo(() =>
{
return GetConfigurationValue<number>('ui.header.images.count', 30);
}, []);
const baseUrl = useMemo(() =>
{
return GetConfigurationValue<string>('ui.header.images.url', 'https://image.webbo.city/image/headerImage/image{id}.gif');
}, []);
const images = useMemo(() =>
{
const result: string[] = [];
for(let i = 1; i <= imageCount; i++)
{
result.push(baseUrl.replace('{id}', String(i)));
}
return result;
}, [ imageCount, baseUrl ]);
const onImageSelect = useCallback((url: string) =>
{
updateSettings({
colorMode: 'image',
headerImageUrl: url
});
}, [ updateSettings ]);
return (
<div className="grid grid-cols-8 gap-1 p-2 overflow-auto max-h-[400px]">
{ images.map((url, i) => (
<div
key={ i }
className={ `w-[75px] h-[75px] rounded cursor-pointer border-2 transition-all hover:scale-105 ${ (settings.colorMode === 'image' && settings.headerImageUrl === url) ? 'border-white shadow-lg' : 'border-transparent' }` }
style={ {
backgroundImage: `url(${ url })`,
backgroundSize: 'cover',
backgroundPosition: 'center'
} }
onClick={ () => onImageSelect(url) }
/>
)) }
</div>
);
};
@@ -0,0 +1,107 @@
import { GetSessionDataManager, HabboClubLevelEnum } from '@nitrots/nitro-renderer';
import { FC, useCallback, useMemo, useState } from 'react';
import { GetClubMemberLevel, GetConfigurationValue } from '../../api';
import { Base, Flex, Grid, LayoutCurrencyIcon, NitroCardTabsItemView, NitroCardTabsView, Text } from '../../common';
import { useRoom } from '../../hooks';
interface ItemData
{
id: number;
isHcOnly: boolean;
minRank: number;
isAmbassadorOnly: boolean;
selectable: boolean;
}
const SUB_TABS = [ 'backgrounds', 'stands', 'overlays' ] as const;
type SubTabType = typeof SUB_TABS[number];
const SUB_TAB_LABELS: Record<SubTabType, string> = {
backgrounds: 'Sfondi',
stands: 'Basi',
overlays: 'Overlay'
};
export const InterfaceProfileTabView: FC<{}> = () =>
{
const [ activeSubTab, setActiveSubTab ] = useState<SubTabType>('backgrounds');
const [ selectedBackground, setSelectedBackground ] = useState<number>(0);
const [ selectedStand, setSelectedStand ] = useState<number>(0);
const [ selectedOverlay, setSelectedOverlay ] = useState<number>(0);
const { roomSession } = useRoom();
const userData = useMemo(() => ({
isHcMember: GetClubMemberLevel() >= HabboClubLevelEnum.CLUB,
securityLevel: GetSessionDataManager().canChangeName,
isAmbassador: GetSessionDataManager().isAmbassador
}), []);
const processData = useCallback((configData: any[], dataType: string): ItemData[] =>
{
if(!configData?.length) return [];
return configData
.filter(item =>
{
const meetsRank = userData.securityLevel >= item.minRank;
const ambassadorEligible = !item.isAmbassadorOnly || userData.isAmbassador;
return item.isHcOnly || (meetsRank && ambassadorEligible);
})
.map(item => ({ id: item[`${ dataType }Id`], ...item, selectable: !item.isHcOnly || userData.isHcMember }));
}, [ userData ]);
const allData = useMemo(() => ({
backgrounds: processData(GetConfigurationValue('backgrounds.data'), 'background'),
stands: processData(GetConfigurationValue('stands.data'), 'stand'),
overlays: processData(GetConfigurationValue('overlays.data'), 'overlay')
}), [ processData ]);
const handleSelection = useCallback((id: number) =>
{
if(!roomSession) return;
const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay };
const currentValues = { backgrounds: selectedBackground, stands: selectedStand, overlays: selectedOverlay };
setters[activeSubTab](id);
const newValues = { ...currentValues, [activeSubTab]: id };
roomSession.sendBackgroundMessage(newValues.backgrounds, newValues.stands, newValues.overlays);
}, [ activeSubTab, roomSession, selectedBackground, selectedStand, selectedOverlay ]);
const renderItem = useCallback((item: ItemData, type: string) => (
<Flex
pointer
position="relative"
key={ item.id }
onClick={ () => item.selectable && handleSelection(item.id) }
className={ item.selectable ? '' : 'non-selectable' }
>
<Base className={ `profile-${ type } ${ type }-${ item.id }` } />
{ item.isHcOnly && <LayoutCurrencyIcon position="absolute" className="top-1 inset-e-1" type="hc" /> }
</Flex>
), [ handleSelection ]);
return (
<Flex column gap={ 1 }>
<Flex gap={ 1 } className="justify-center">
{ SUB_TABS.map(tab => (
<button
key={ tab }
className={ `px-3 py-1 rounded text-sm cursor-pointer transition-colors ${ activeSubTab === tab ? 'bg-primary text-white' : 'bg-card-grid-item text-black hover:bg-card-grid-item-active' }` }
onClick={ () => setActiveSubTab(tab) }
>
{ SUB_TAB_LABELS[tab] }
</button>
)) }
</Flex>
{ !roomSession && (
<Text bold center className="text-black py-4">Entra in una stanza per modificare il profilo</Text>
) }
{ roomSession && (
<Grid gap={ 1 } columnCount={ 7 } overflow="auto" className="max-h-[300px]">
{ allData[activeSubTab].map(item => renderItem(item, activeSubTab.slice(0, -1))) }
</Grid>
) }
</Flex>
);
};
@@ -0,0 +1,74 @@
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsView, NitroCardTabsItemView, NitroCardView } from '../../common';
import { InterfaceColorTabView } from './InterfaceColorTabView';
import { InterfaceProfileTabView } from './InterfaceProfileTabView';
const TABS = [ 'color', 'profile' ] as const;
type TabType = typeof TABS[number];
const TAB_LABELS: Record<TabType, string> = {
color: 'Colore',
profile: 'Sfondo profilo'
};
export const InterfaceSettingsView: FC<{}> = () =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ currentTab, setCurrentTab ] = useState<TabType>('color');
useEffect(() =>
{
const linkTracker: ILinkEventTracker = {
linkReceived: (url: string) =>
{
const parts = url.split('/');
if(parts.length < 2) return;
switch(parts[1])
{
case 'show':
setIsVisible(true);
return;
case 'hide':
setIsVisible(false);
return;
case 'toggle':
setIsVisible(prev => !prev);
return;
case 'profile':
setCurrentTab('profile');
setIsVisible(true);
return;
}
},
eventUrlPrefix: 'interface-settings/'
};
AddLinkEventTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, []);
if(!isVisible) return null;
return (
<NitroCardView uniqueKey="interface-settings" className="min-w-[535px] max-w-[700px]">
<NitroCardHeaderView headerText="Interfaccia" onCloseClick={ () => setIsVisible(false) } />
<NitroCardTabsView>
{ TABS.map(tab => (
<NitroCardTabsItemView
key={ tab }
isActive={ currentTab === tab }
onClick={ () => setCurrentTab(tab) }
>
{ TAB_LABELS[tab] }
</NitroCardTabsItemView>
)) }
</NitroCardTabsView>
<NitroCardContentView>
{ currentTab === 'color' && <InterfaceColorTabView /> }
{ currentTab === 'profile' && <InterfaceProfileTabView /> }
</NitroCardContentView>
</NitroCardView>
);
};
+14 -6
View File
@@ -2,21 +2,29 @@ import { FC } from 'react';
import { Base, Column, Text } from '../../common';
interface LoadingViewProps {
isError: boolean;
message: string;
isError?: boolean;
message?: string;
}
export const LoadingView: FC<LoadingViewProps> = props => {
const { isError = false, message = '' } = props;
return (
<Column fullHeight position="relative" className="relative z-[100] bg-[radial-gradient(#1d1a24,#003a6b)]">
<Base fullHeight className="container h-100">
<Column fullHeight alignItems="center" justifyContent="center">
<Base className="absolute inset-0 m-auto w-[84px] h-[84px] [zoom:1.5] [image-rendering:pixelated] bg-[url('@/assets/images/loading/loading.gif')] bg-no-repeat bg-left-top" />
<Base className="absolute top-[20px] left-[20px] z-[2] w-[150px] h-[100px] bg-[url('@/assets/images/notifications/coolui.png')] bg-no-repeat bg-left-top" />
{ !isError &&
<Base className="absolute inset-0 m-auto w-[84px] h-[84px] [zoom:1.5] [image-rendering:pixelated] bg-[url('@/assets/images/loading/loading.gif')] bg-no-repeat bg-left-top" /> }
<Base className="absolute top-[20px] left-[20px] z-[2] w-[150px] h-[100px] bg-[url('@/assets/images/notifications/nitro_v3.png')] bg-no-repeat bg-left-top" />
{ isError && (message && message.length) ?
<Base className="fs-4 absolute bottom-[20px] left-1/2 z-[3] -translate-x-1/2 [text-shadow:0px_4px_4px_rgba(0,0,0,0.25)]">{ message }</Base>
<Column alignItems="center" className="absolute bottom-[20px] left-1/2 z-[3] -translate-x-1/2 max-w-[80%]" gap={ 2 }>
<Text fontSizeCustom={ 20 } variant="white" className="text-center [text-shadow:0px_4px_4px_rgba(0,0,0,0.25)]">
Something went wrong while loading
</Text>
<Base className="px-4 py-3 rounded-lg bg-black/40 text-[#ff6b6b] text-sm font-mono text-center break-words whitespace-pre-wrap max-w-[600px]">
{ message }
</Base>
</Column>
:
<Text fontSizeCustom={32} variant="white" className="absolute bottom-[20px] left-1/2 z-[3] -translate-x-1/2 [text-shadow:0px_4px_4px_rgba(0,0,0,0.25)]">
The hotel is loading ...
+6 -6
View File
@@ -1,6 +1,6 @@
import { AddLinkEventTracker, CreateLinkEvent, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomId, RoomObjectCategory, RoomObjectType } from '@nitrots/nitro-renderer';
import { FC, useEffect, useRef, useState } from 'react';
import { GetRoomSession, ISelectedUser } from '../../api';
import { GetRoomSession, ISelectedUser, LocalizeText } from '../../api';
import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common';
import { useModTools, useNitroEvent, useObjectSelectedEvent } from '../../hooks';
import { ModToolsChatlogView } from './views/room/ModToolsChatlogView';
@@ -125,23 +125,23 @@ export const ModToolsView: FC<{}> = props =>
<>
{ isVisible &&
<NitroCardView className="nitro-mod-tools min-w-[200px]" theme="primary-slim" uniqueKey="mod-tools" windowPosition={ DraggableWindowPosition.TOP_LEFT } >
<NitroCardHeaderView headerText={ 'Mod Tools' } onCloseClick={ event => setIsVisible(false) } />
<NitroCardHeaderView headerText={ LocalizeText('moderation.modtools.title') } onCloseClick={ event => setIsVisible(false) } />
<NitroCardContentView className="text-black" gap={ 2 }>
<Button active={ isRoomInfoOpen } disabled={ (currentRoomId <= 0) } gap={ 2 } justifyContent="start" onClick={ event => CreateLinkEvent(`mod-tools/toggle-room-info/${ currentRoomId }`) }>
<div className="nitro-icon icon-small-room shrink-0" /> Room Tool
<div className="nitro-icon icon-small-room shrink-0" /> { LocalizeText('moderation.modtools.roomtool') }
</Button>
<Button active={ isRoomChatlogOpen } disabled={ (currentRoomId <= 0) } gap={ 2 } innerRef={ elementRef } justifyContent="start" onClick={ event => CreateLinkEvent(`mod-tools/toggle-room-chatlog/${ currentRoomId }`) }>
<div className="nitro-icon icon-chat-history shrink-0" /> Chatlog Tool
<div className="nitro-icon icon-chat-history shrink-0" /> { LocalizeText('moderation.modtools.roomchatlogs') }
</Button>
<Button active={ !!isUserInfoOpen } disabled={ !selectedUser } gap={ 2 } justifyContent="start" onClick={ () => CreateLinkEvent(`mod-tools/toggle-user-info/${ selectedUser.userId }`) }>
<div className="nitro-icon icon-user shrink-0" />
{ selectedUser
? <span className="truncate">{ selectedUser.username }</span>
: <span className="opacity-50 italic">Select a user</span>
: <span className="opacity-50 italic">{ LocalizeText('moderation.modtools.userinfo') }</span>
}
</Button>
<Button active={ isTicketsVisible } gap={ 2 } justifyContent="start" onClick={ () => setIsTicketsVisible(prevValue => !prevValue) }>
<div className="nitro-icon icon-tickets shrink-0" /> Report Tool
<div className="nitro-icon icon-tickets shrink-0" /> { LocalizeText('moderation.modtools.tickets') }
</Button>
</NitroCardContentView>
</NitroCardView> }
@@ -1,6 +1,6 @@
import { ChatRecordData, CreateLinkEvent } from '@nitrots/nitro-renderer';
import { FC, useMemo } from 'react';
import { TryVisitRoom } from '../../../../api';
import { LocalizeText, TryVisitRoom } from '../../../../api';
import { Button, Column, Flex, Grid, InfiniteScroll, Text } from '../../../../common';
import { useModTools } from '../../../../hooks';
import { ChatlogRecord } from './ChatlogRecord';
@@ -49,8 +49,8 @@ export const ChatlogView: FC<ChatlogViewProps> = props =>
<Flex alignItems="center" className="bg-muted rounded p-2" gap={ 2 } justifyContent="between">
<Text bold truncate>{ props.roomName }</Text>
<div className="flex gap-1 shrink-0">
<Button size="sm" onClick={ event => TryVisitRoom(props.roomId) }>Visit</Button>
<Button size="sm" onClick={ event => openRoomInfo(props.roomId) }>Room Tools</Button>
<Button size="sm" onClick={ event => TryVisitRoom(props.roomId) }>{ LocalizeText('moderation.chatlog.visit') }</Button>
<Button size="sm" onClick={ event => openRoomInfo(props.roomId) }>{ LocalizeText('moderation.chatlog.roomtools') }</Button>
</div>
</Flex>
);
@@ -61,9 +61,9 @@ export const ChatlogView: FC<ChatlogViewProps> = props =>
<Column fit gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid className="text-black font-bold border-bottom pb-1 text-[11px] uppercase opacity-60 tracking-wider" gap={ 1 }>
<div className="col-span-2">Time</div>
<div className="col-span-3">User</div>
<div className="col-span-7">Message</div>
<div className="col-span-2">{ LocalizeText('moderation.chatlog.col.time') }</div>
<div className="col-span-3">{ LocalizeText('moderation.chatlog.col.user') }</div>
<div className="col-span-7">{ LocalizeText('moderation.chatlog.col.message') }</div>
</Grid>
</Column>
{ (records && (records.length > 0)) &&
@@ -1,6 +1,6 @@
import { ChatRecordData, GetRoomChatlogMessageComposer, RoomChatlogEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { SendMessageComposer } from '../../../../api';
import { LocalizeText, SendMessageComposer } from '../../../../api';
import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
import { ChatlogView } from '../chatlog/ChatlogView';
@@ -34,7 +34,7 @@ export const ModToolsChatlogView: FC<ModToolsChatlogViewProps> = props =>
return (
<NitroCardView className="nitro-mod-tools-chatlog min-w-[400px] max-h-[500px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ `Room Chatlog` } onCloseClick={ onCloseClick } />
<NitroCardHeaderView headerText={ LocalizeText('moderation.chatlog.room') } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black" overflow="auto">
{ roomChatlog &&
<ChatlogView records={ [ roomChatlog ] } /> }
@@ -1,6 +1,6 @@
import { CreateLinkEvent, GetModeratorRoomInfoMessageComposer, ModerateRoomMessageComposer, ModeratorActionMessageComposer, ModeratorRoomInfoEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { SendMessageComposer, TryVisitRoom } from '../../../../api';
import { LocalizeText, SendMessageComposer, TryVisitRoom } from '../../../../api';
import { Button, Column, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
@@ -70,7 +70,7 @@ export const ModToolsRoomView: FC<ModToolsRoomViewProps> = props =>
return (
<NitroCardView className="nitro-mod-tools-room min-w-[280px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ 'Room Info' } onCloseClick={ event => onCloseClick() } />
<NitroCardHeaderView headerText={ LocalizeText('moderation.roomtool.info.title') } onCloseClick={ event => onCloseClick() } />
<NitroCardContentView className="text-black" gap={ 2 }>
{ name &&
<div className="bg-muted rounded px-2 py-1.5 text-center">
@@ -80,41 +80,41 @@ export const ModToolsRoomView: FC<ModToolsRoomViewProps> = props =>
<div className="flex gap-2">
<Column grow gap={ 1 }>
<div className="flex items-center gap-1">
<Text bold className="opacity-60 shrink-0">Owner:</Text>
<Text bold className="opacity-60 shrink-0">{ LocalizeText('moderation.roomtool.roomowner.title') }</Text>
<Text bold pointer truncate underline onClick={ () => CreateLinkEvent(`mod-tools/open-user-info/${ ownerId }`) }>{ ownerName }</Text>
</div>
<div className="flex items-center gap-1">
<Text bold className="opacity-60 shrink-0">Users in room:</Text>
<Text bold className="opacity-60 shrink-0">{ LocalizeText('moderation.roomtool.usersinroom.title') }</Text>
<Text>{ usersInRoom }</Text>
</div>
<div className="flex items-center gap-1">
<Text bold className="opacity-60 shrink-0">Owner here:</Text>
<Text className={ ownerInRoom ? 'text-green-700' : 'text-red-700' }>{ ownerInRoom ? 'Yes' : 'No' }</Text>
<Text bold className="opacity-60 shrink-0">{ LocalizeText('moderation.roomtool.ownerinroom.title') }</Text>
<Text className={ ownerInRoom ? 'text-green-700' : 'text-red-700' }>{ ownerInRoom ? LocalizeText('moderation.roomtool.true.title') : LocalizeText('moderation.roomtool.false.title') }</Text>
</div>
</Column>
<div className="flex flex-col gap-1 shrink-0">
<Button onClick={ event => TryVisitRoom(roomId) }>Visit Room</Button>
<Button onClick={ event => CreateLinkEvent(`mod-tools/open-room-chatlog/${ roomId }`) }>Chatlog</Button>
<Button onClick={ event => TryVisitRoom(roomId) }>{ LocalizeText('moderation.roomtool.button.visit.title') }</Button>
<Button onClick={ event => CreateLinkEvent(`mod-tools/open-room-chatlog/${ roomId }`) }>{ LocalizeText('moderation.modtools.roomchatlogs') }</Button>
</div>
</div>
<Column className="bg-muted rounded p-2" gap={ 1 }>
<div className="flex items-center gap-2">
<input checked={ kickUsers } className="form-check-input" type="checkbox" onChange={ event => setKickUsers(event.target.checked) } />
<Text small>Kick everyone out</Text>
<Text small>{ LocalizeText('moderation.roomtool.kickall.title') }</Text>
</div>
<div className="flex items-center gap-2">
<input checked={ lockRoom } className="form-check-input" type="checkbox" onChange={ event => setLockRoom(event.target.checked) } />
<Text small>Enable the doorbell</Text>
<Text small>{ LocalizeText('moderation.roomtool.closeroom.title') }</Text>
</div>
<div className="flex items-center gap-2">
<input checked={ changeRoomName } className="form-check-input" type="checkbox" onChange={ event => setChangeRoomName(event.target.checked) } />
<Text small>Change room name</Text>
<Text small>{ LocalizeText('moderation.roomtool.inappropiatename.title') }</Text>
</div>
</Column>
<textarea className="min-h-[60px] px-2 py-1.5 rounded text-sm border border-black/10" placeholder="Type a mandatory message..." value={ message } onChange={ event => setMessage(event.target.value) }></textarea>
<textarea className="min-h-[60px] px-2 py-1.5 rounded text-sm border border-black/10" placeholder={ LocalizeText('moderation.roomtool.presets.title') } value={ message } onChange={ event => setMessage(event.target.value) }></textarea>
<div className="flex gap-2">
<Button className="grow" variant="danger" onClick={ event => handleClick('send_message') }>Send Caution</Button>
<Button className="grow" onClick={ event => handleClick('alert_only') }>Send Alert</Button>
<Button className="grow" variant="danger" onClick={ event => handleClick('send_message') }>{ LocalizeText('moderation.roomtool.button.caution.title') }</Button>
<Button className="grow" onClick={ event => handleClick('alert_only') }>{ LocalizeText('moderation.roomtool.button.message.title') }</Button>
</div>
</NitroCardContentView>
</NitroCardView>
@@ -1,6 +1,6 @@
import { CfhChatlogData, CfhChatlogEvent, GetCfhChatlogMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { SendMessageComposer } from '../../../../api';
import { LocalizeText, SendMessageComposer } from '../../../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
import { ChatlogView } from '../chatlog/ChatlogView';
@@ -32,7 +32,7 @@ export const CfhChatlogView: FC<CfhChatlogViewProps> = props =>
return (
<NitroCardView className="nitro-mod-tools-chatlog" theme="primary-slim">
<NitroCardHeaderView headerText={ 'Issue Chatlog' } onCloseClick={ onCloseClick } />
<NitroCardHeaderView headerText={ LocalizeText('moderation.chatlog.issue') } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black">
{ chatlogData && <ChatlogView records={ [ chatlogData.chatRecord ] } /> }
</NitroCardContentView>
@@ -35,33 +35,33 @@ export const ModToolsIssueInfoView: FC<IssueInfoViewProps> = props =>
return (
<>
<NitroCardView className="nitro-mod-tools-handle-issue" theme="primary-slim">
<NitroCardHeaderView headerText={ 'Resolving issue ' + issueId } onCloseClick={ () => onIssueInfoClosed(issueId) } />
<NitroCardHeaderView headerText={ LocalizeText('moderation.issue.resolving', [ 'id' ], [ String(issueId) ]) } onCloseClick={ () => onIssueInfoClosed(issueId) } />
<NitroCardContentView className="text-black">
<Text fontSize={ 4 }>Issue Information</Text>
<Text fontSize={ 4 }>{ LocalizeText('moderation.issue.info') }</Text>
<Grid overflow="auto">
<Column size={ 8 }>
<table className="table table-striped table-sm table-text-small text-black m-0">
<tbody>
<tr>
<th>Source</th>
<th>{ LocalizeText('moderation.issue.source') }</th>
<td>{ GetIssueCategoryName(ticket.categoryId) }</td>
</tr>
<tr>
<th>Category</th>
<th>{ LocalizeText('moderation.issue.category') }</th>
<td className="text-break">{ LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId) }</td>
</tr>
<tr>
<th>Description</th>
<th>{ LocalizeText('moderation.issue.description') }</th>
<td className="text-break">{ ticket.message }</td>
</tr>
<tr>
<th>Caller</th>
<th>{ LocalizeText('moderation.issue.caller') }</th>
<td>
<Text bold pointer underline onClick={ event => openUserInfo(ticket.reporterUserId) }>{ ticket.reporterUserName }</Text>
</td>
</tr>
<tr>
<th>Reported User</th>
<th>{ LocalizeText('moderation.issue.reported') }</th>
<td>
<Text bold pointer underline onClick={ event => openUserInfo(ticket.reportedUserId) }>{ ticket.reportedUserName }</Text>
</td>
@@ -70,11 +70,11 @@ export const ModToolsIssueInfoView: FC<IssueInfoViewProps> = props =>
</table>
</Column>
<Column gap={ 1 } size={ 4 }>
<Button variant="secondary" onClick={ () => setcfhChatlogOpen(!cfhChatlogOpen) }>Chatlog</Button>
<Button onClick={ event => closeIssue(CloseIssuesMessageComposer.RESOLUTION_USELESS) }>Close as useless</Button>
<Button variant="danger" onClick={ event => closeIssue(CloseIssuesMessageComposer.RESOLUTION_ABUSIVE) }>Close as abusive</Button>
<Button variant="success" onClick={ event => closeIssue(CloseIssuesMessageComposer.RESOLUTION_RESOLVED) }>Close as resolved</Button>
<Button variant="secondary" onClick={ event => releaseIssue(issueId) } >Release</Button>
<Button variant="secondary" onClick={ () => setcfhChatlogOpen(!cfhChatlogOpen) }>{ LocalizeText('moderation.issue.chatlog') }</Button>
<Button onClick={ event => closeIssue(CloseIssuesMessageComposer.RESOLUTION_USELESS) }>{ LocalizeText('moderation.issue.close.useless') }</Button>
<Button variant="danger" onClick={ event => closeIssue(CloseIssuesMessageComposer.RESOLUTION_ABUSIVE) }>{ LocalizeText('moderation.issue.close.abusive') }</Button>
<Button variant="success" onClick={ event => closeIssue(CloseIssuesMessageComposer.RESOLUTION_RESOLVED) }>{ LocalizeText('moderation.issue.close.resolved') }</Button>
<Button variant="secondary" onClick={ event => releaseIssue(issueId) } >{ LocalizeText('moderation.issue.release') }</Button>
</Column>
</Grid>
</NitroCardContentView>
@@ -1,6 +1,6 @@
import { IssueMessageData, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useRef } from 'react';
import { SendMessageComposer } from '../../../../api';
import { LocalizeText, SendMessageComposer } from '../../../../api';
import { Button, Column, Grid } from '../../../../common';
interface ModToolsMyIssuesTabViewProps
@@ -28,9 +28,9 @@ export const ModToolsMyIssuesTabView: FC<ModToolsMyIssuesTabViewProps> = props =
<Column gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid className="text-black font-bold border-bottom pb-1" gap={ 1 }>
<div className="col-span-2">Type</div>
<div className="col-span-3">Room/Player</div>
<div className="col-span-3">Opened</div>
<div className="col-span-2">{ LocalizeText('moderation.tickets.col.type') }</div>
<div className="col-span-3">{ LocalizeText('moderation.tickets.col.roomPlayer') }</div>
<div className="col-span-3">{ LocalizeText('moderation.tickets.col.opened') }</div>
<div className="col-span-2"></div>
<div className="col-span-2"></div>
</Grid>
@@ -44,10 +44,10 @@ export const ModToolsMyIssuesTabView: FC<ModToolsMyIssuesTabViewProps> = props =
<div className="col-span-3">{ issue.reportedUserName }</div>
<div className="col-span-3">{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }</div>
<div className="col-span-2">
<Button variant="primary" onClick={ event => handleIssue(issue.issueId) }>Handle</Button>
<Button variant="primary" onClick={ event => handleIssue(issue.issueId) }>{ LocalizeText('moderation.tickets.handle') }</Button>
</div>
<div className="col-span-2">
<Button variant="danger" onClick={ () => releaseIssue(issue.issueId) }>Release</Button>
<Button variant="danger" onClick={ () => releaseIssue(issue.issueId) }>{ LocalizeText('moderation.tickets.release') }</Button>
</div>
</Grid>
);
@@ -1,6 +1,6 @@
import { IssueMessageData, PickIssuesMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useRef } from 'react';
import { SendMessageComposer } from '../../../../api';
import { LocalizeText, SendMessageComposer } from '../../../../api';
import { Button, Column, Grid } from '../../../../common';
interface ModToolsOpenIssuesTabViewProps
@@ -27,9 +27,9 @@ export const ModToolsOpenIssuesTabView: FC<ModToolsOpenIssuesTabViewProps> = pro
<Column gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid className="text-black font-bold border-bottom pb-1" gap={ 1 }>
<div className="col-span-2">Type</div>
<div className="col-span-3">Room/Player</div>
<div className="col-span-4">Opened</div>
<div className="col-span-2">{ LocalizeText('moderation.tickets.col.type') }</div>
<div className="col-span-3">{ LocalizeText('moderation.tickets.col.roomPlayer') }</div>
<div className="col-span-4">{ LocalizeText('moderation.tickets.col.opened') }</div>
<div className="col-span-3"></div>
</Grid>
</Column>
@@ -42,7 +42,7 @@ export const ModToolsOpenIssuesTabView: FC<ModToolsOpenIssuesTabViewProps> = pro
<div className="col-span-3">{ issue.reportedUserName }</div>
<div className="col-span-4">{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }</div>
<div className="col-span-3">
<Button variant="success" onClick={ () => pickIssue(issue.issueId) }>Pick Issue</Button>
<Button variant="success" onClick={ () => pickIssue(issue.issueId) }>{ LocalizeText('moderation.tickets.pick') }</Button>
</div>
</Grid>
);
@@ -1,5 +1,6 @@
import { IssueMessageData } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { LocalizeText } from '../../../../api';
import { Column, Grid } from '../../../../common';
interface ModToolsPickedIssuesTabViewProps
@@ -15,10 +16,10 @@ export const ModToolsPickedIssuesTabView: FC<ModToolsPickedIssuesTabViewProps> =
<Column gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid className="text-black font-bold border-bottom pb-1" gap={ 1 }>
<div className="col-span-2">Type</div>
<div className="col-span-3">Room/Player</div>
<div className="col-span-4">Opened</div>
<div className="col-span-3">Picker</div>
<div className="col-span-2">{ LocalizeText('moderation.tickets.col.type') }</div>
<div className="col-span-3">{ LocalizeText('moderation.tickets.col.roomPlayer') }</div>
<div className="col-span-4">{ LocalizeText('moderation.tickets.col.opened') }</div>
<div className="col-span-3">{ LocalizeText('moderation.tickets.col.picker') }</div>
</Grid>
</Column>
<Column className="striped-children" gap={ 0 } overflow="auto">
@@ -1,5 +1,6 @@
import { GetSessionDataManager, IssueMessageData } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { LocalizeText } from '../../../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../common';
import { useModTools } from '../../../../hooks';
import { ModToolsIssueInfoView } from './ModToolsIssueInfoView';
@@ -12,10 +13,10 @@ interface ModToolsTicketsViewProps
onCloseClick: () => void;
}
const TABS: string[] = [
'Open Issues',
'My Issues',
'Picked Issues'
const TAB_KEYS: string[] = [
'moderation.tickets.open',
'moderation.tickets.my',
'moderation.tickets.picked'
];
export const ModToolsTicketsView: FC<ModToolsTicketsViewProps> = props =>
@@ -71,12 +72,12 @@ export const ModToolsTicketsView: FC<ModToolsTicketsViewProps> = props =>
return (
<>
<NitroCardView className="nitro-mod-tools-tickets">
<NitroCardHeaderView headerText={ 'Tickets' } onCloseClick={ onCloseClick } />
<NitroCardHeaderView headerText={ LocalizeText('moderation.tickets.title') } onCloseClick={ onCloseClick } />
<NitroCardTabsView>
{ TABS.map((tab, index) =>
{ TAB_KEYS.map((tabKey, index) =>
{
return (<NitroCardTabsItemView key={ index } isActive={ (currentTab === index) } onClick={ event => setCurrentTab(index) }>
{ tab }
{ LocalizeText(tabKey) }
</NitroCardTabsItemView>);
}) }
</NitroCardTabsView>
@@ -1,6 +1,6 @@
import { ChatRecordData, GetUserChatlogMessageComposer, UserChatlogEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { SendMessageComposer } from '../../../../api';
import { LocalizeText, SendMessageComposer } from '../../../../api';
import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
import { ChatlogView } from '../chatlog/ChatlogView';
@@ -34,7 +34,7 @@ export const ModToolsUserChatlogView: FC<ModToolsUserChatlogViewProps> = props =
return (
<NitroCardView className="nitro-mod-tools-chatlog" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ `User Chatlog: ${ username || '' }` } onCloseClick={ onCloseClick } />
<NitroCardHeaderView headerText={ LocalizeText('moderation.chatlog.user', [ 'username' ], [ username || '' ]) } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black h-full">
{ userChatlog &&
<ChatlogView records={ userChatlog } /> }
@@ -11,18 +11,18 @@ interface ModToolsUserModActionViewProps
}
const MOD_ACTION_DEFINITIONS = [
new ModActionDefinition(1, 'Alert', ModActionDefinition.ALERT, 1, 0),
new ModActionDefinition(2, 'Mute 1h', ModActionDefinition.MUTE, 2, 0),
new ModActionDefinition(3, 'Ban 18h', ModActionDefinition.BAN, 3, 0),
new ModActionDefinition(4, 'Ban 7 days', ModActionDefinition.BAN, 4, 0),
new ModActionDefinition(5, 'Ban 30 days (step 1)', ModActionDefinition.BAN, 5, 0),
new ModActionDefinition(7, 'Ban 30 days (step 2)', ModActionDefinition.BAN, 7, 0),
new ModActionDefinition(6, 'Ban 100 years', ModActionDefinition.BAN, 6, 0),
new ModActionDefinition(106, 'Ban avatar-only 100 years', ModActionDefinition.BAN, 6, 0),
new ModActionDefinition(101, 'Kick', ModActionDefinition.KICK, 0, 0),
new ModActionDefinition(102, 'Lock trade 1 week', ModActionDefinition.TRADE_LOCK, 0, 168),
new ModActionDefinition(104, 'Lock trade permanent', ModActionDefinition.TRADE_LOCK, 0, 876000),
new ModActionDefinition(105, 'Message', ModActionDefinition.MESSAGE, 0, 0),
new ModActionDefinition(1, 'moderation.modaction.alert', ModActionDefinition.ALERT, 1, 0),
new ModActionDefinition(2, 'moderation.modaction.mute1h', ModActionDefinition.MUTE, 2, 0),
new ModActionDefinition(3, 'moderation.modaction.ban18h', ModActionDefinition.BAN, 3, 0),
new ModActionDefinition(4, 'moderation.modaction.ban7days', ModActionDefinition.BAN, 4, 0),
new ModActionDefinition(5, 'moderation.modaction.ban30days.step1', ModActionDefinition.BAN, 5, 0),
new ModActionDefinition(7, 'moderation.modaction.ban30days.step2', ModActionDefinition.BAN, 7, 0),
new ModActionDefinition(6, 'moderation.modaction.ban100years', ModActionDefinition.BAN, 6, 0),
new ModActionDefinition(106, 'moderation.modaction.banavataronly100years', ModActionDefinition.BAN, 6, 0),
new ModActionDefinition(101, 'moderation.modaction.kick', ModActionDefinition.KICK, 0, 0),
new ModActionDefinition(102, 'moderation.modaction.locktrade1week', ModActionDefinition.TRADE_LOCK, 0, 168),
new ModActionDefinition(104, 'moderation.modaction.locktradepermanent', ModActionDefinition.TRADE_LOCK, 0, 876000),
new ModActionDefinition(105, 'moderation.modaction.message', ModActionDefinition.MESSAGE, 0, 0),
];
export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = props =>
@@ -60,7 +60,7 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
const category = topics[selectedTopic];
if(selectedTopic === -1) errorMessage = 'You must select a CFH topic';
if(selectedTopic === -1) errorMessage = LocalizeText('moderation.modaction.error.notopic');
if(errorMessage) return sendAlert(errorMessage);
@@ -82,10 +82,10 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
const category = topics[selectedTopic];
const sanction = MOD_ACTION_DEFINITIONS[selectedAction];
if((selectedTopic === -1) || (selectedAction === -1)) errorMessage = 'You must select a CFH topic and Sanction';
else if(!settings || !settings.cfhPermission) errorMessage = 'You do not have permission to do this';
else if(!category) errorMessage = 'You must select a CFH topic';
else if(!sanction) errorMessage = 'You must select a sanction';
if((selectedTopic === -1) || (selectedAction === -1)) errorMessage = LocalizeText('moderation.modaction.error.notopicorsanction');
else if(!settings || !settings.cfhPermission) errorMessage = LocalizeText('moderation.modaction.error.nopermission');
else if(!category) errorMessage = LocalizeText('moderation.modaction.error.notopic');
else if(!sanction) errorMessage = LocalizeText('moderation.modaction.error.nosanction');
if(errorMessage)
{
@@ -101,7 +101,7 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
case ModActionDefinition.ALERT: {
if(!settings.alertPermission)
{
sendAlert('You have insufficient permissions');
sendAlert(LocalizeText('moderation.modaction.error.nopermission'));
return;
}
@@ -115,7 +115,7 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
case ModActionDefinition.BAN: {
if(!settings.banPermission)
{
sendAlert('You have insufficient permissions');
sendAlert(LocalizeText('moderation.modaction.error.nopermission'));
return;
}
@@ -126,7 +126,7 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
case ModActionDefinition.KICK: {
if(!settings.kickPermission)
{
sendAlert('You have insufficient permissions');
sendAlert(LocalizeText('moderation.modaction.error.nopermission'));
return;
}
@@ -142,7 +142,7 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
case ModActionDefinition.MESSAGE: {
if(message.trim().length === 0)
{
sendAlert('Please write a message to user');
sendAlert(LocalizeText('moderation.modaction.error.emptymessage'));
return;
}
@@ -161,23 +161,23 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
return (
<NitroCardView className="nitro-mod-tools-user-action" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ 'Mod Action: ' + (user ? user.username : '') } onCloseClick={ () => onCloseClick() } />
<NitroCardHeaderView headerText={ LocalizeText('moderation.modaction.title', [ 'username' ], [ user ? user.username : '' ]) } onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black">
<select className="form-select form-select-sm" value={ selectedTopic } onChange={ event => setSelectedTopic(parseInt(event.target.value)) }>
<option disabled value={ -1 }>CFH Topic</option>
<option disabled value={ -1 }>{ LocalizeText('moderation.modaction.cfhtopic') }</option>
{ topics.map((topic, index) => <option key={ index } value={ index }>{ LocalizeText('help.cfh.topic.' + topic.id) }</option>) }
</select>
<select className="form-select form-select-sm" value={ selectedAction } onChange={ event => setSelectedAction(parseInt(event.target.value)) }>
<option disabled value={ -1 }>Sanction Type</option>
{ MOD_ACTION_DEFINITIONS.map((action, index) => <option key={ index } value={ index }>{ action.name }</option>) }
<option disabled value={ -1 }>{ LocalizeText('moderation.modaction.sanctiontype') }</option>
{ MOD_ACTION_DEFINITIONS.map((action, index) => <option key={ index } value={ index }>{ LocalizeText(action.name) }</option>) }
</select>
<div className="flex flex-col gap-1">
<Text small>Optional message type, overrides default</Text>
<Text small>{ LocalizeText('moderation.modaction.message.hint') }</Text>
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem]" value={ message } onChange={ event => setMessage(event.target.value) } />
</div>
<Flex gap={ 1 } justifyContent="between">
<Button variant="primary" onClick={ sendDefaultSanction }>Default Sanction</Button>
<Button variant="success" onClick={ sendSanction }>Sanction</Button>
<Button variant="primary" onClick={ sendDefaultSanction }>{ LocalizeText('moderation.modaction.defaultsanction') }</Button>
<Button variant="success" onClick={ sendSanction }>{ LocalizeText('moderation.modaction.sanction') }</Button>
</Flex>
</NitroCardContentView>
</NitroCardView>
@@ -1,6 +1,6 @@
import { GetRoomVisitsMessageComposer, RoomVisitsData, RoomVisitsEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { SendMessageComposer, TryVisitRoom } from '../../../../api';
import { LocalizeText, SendMessageComposer, TryVisitRoom } from '../../../../api';
import { Column, DraggableWindowPosition, Grid, InfiniteScroll, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
@@ -33,14 +33,14 @@ export const ModToolsUserRoomVisitsView: FC<ModToolsUserRoomVisitsViewProps> = p
return (
<NitroCardView className="nitro-mod-tools-user-visits" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ 'User Visits' } onCloseClick={ onCloseClick } />
<NitroCardHeaderView headerText={ LocalizeText('moderation.roomvisits.title') } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black" gap={ 1 }>
<Column fullHeight gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid className="text-black font-bold border-bottom pb-1" gap={ 1 }>
<div className="col-span-2">Time</div>
<div className="col-span-7">Room name</div>
<div className="col-span-3">Visit</div>
<div className="col-span-2">{ LocalizeText('moderation.roomvisits.col.time') }</div>
<div className="col-span-7">{ LocalizeText('moderation.roomvisits.col.roomname') }</div>
<div className="col-span-3">{ LocalizeText('moderation.roomvisits.col.visit') }</div>
</Grid>
</Column>
<InfiniteScroll rowRender={ row =>
@@ -49,7 +49,7 @@ export const ModToolsUserRoomVisitsView: FC<ModToolsUserRoomVisitsViewProps> = p
<Grid alignItems="center" className="text-black py-1 border-bottom" fullHeight={ false } gap={ 1 }>
<Text className="col-span-2">{ row.enterHour.toString().padStart(2, '0') }: { row.enterMinute.toString().padStart(2, '0') }</Text>
<Text className="col-span-7">{ row.roomName }</Text>
<Text bold pointer underline className="col-span-3" variant="primary" onClick={ event => TryVisitRoom(row.roomId) }>Visit Room</Text>
<Text bold pointer underline className="col-span-3" variant="primary" onClick={ event => TryVisitRoom(row.roomId) }>{ LocalizeText('moderation.roomvisits.visitroom') }</Text>
</Grid>
);
} } rows={ roomVisitData?.rooms ?? [] } />
@@ -1,6 +1,6 @@
import { ModMessageMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { ISelectedUser, SendMessageComposer } from '../../../../api';
import { ISelectedUser, LocalizeText, SendMessageComposer } from '../../../../api';
import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useNotification } from '../../../../hooks';
@@ -22,7 +22,7 @@ export const ModToolsUserSendMessageView: FC<ModToolsUserSendMessageViewProps> =
{
if(message.trim().length === 0)
{
simpleAlert('Please write a message to user.', null, null, null, 'Error', null);
simpleAlert(LocalizeText('moderation.sendmessage.error.empty'), null, null, null, LocalizeText('moderation.error'), null);
return;
}
@@ -34,11 +34,11 @@ export const ModToolsUserSendMessageView: FC<ModToolsUserSendMessageViewProps> =
return (
<NitroCardView className="nitro-mod-tools-user-message" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ 'Send Message' } onCloseClick={ () => onCloseClick() } />
<NitroCardHeaderView headerText={ LocalizeText('moderation.sendmessage.title') } onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black">
<Text>Message To: { user.username }</Text>
<Text>{ LocalizeText('moderation.sendmessage.to', [ 'username' ], [ user.username ]) }</Text>
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem]" value={ message } onChange={ event => setMessage(event.target.value) }></textarea>
<Button fullWidth onClick={ sendMessage }>Send message</Button>
<Button fullWidth onClick={ sendMessage }>{ LocalizeText('moderation.sendmessage.send') }</Button>
</NitroCardContentView>
</NitroCardView>
);
@@ -27,60 +27,60 @@ export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
return [
{
localeKey: 'modtools.userinfo.userName',
localeKey: 'moderation.userinfo.userName',
value: userInfo.userName,
showOnline: true
},
{
localeKey: 'modtools.userinfo.cfhCount',
localeKey: 'moderation.userinfo.cfhCount',
value: userInfo.cfhCount.toString()
},
{
localeKey: 'modtools.userinfo.abusiveCfhCount',
localeKey: 'moderation.userinfo.abusiveCfhCount',
value: userInfo.abusiveCfhCount.toString()
},
{
localeKey: 'modtools.userinfo.cautionCount',
localeKey: 'moderation.userinfo.cautionCount',
value: userInfo.cautionCount.toString()
},
{
localeKey: 'modtools.userinfo.banCount',
localeKey: 'moderation.userinfo.banCount',
value: userInfo.banCount.toString()
},
{
localeKey: 'modtools.userinfo.lastSanctionTime',
localeKey: 'moderation.userinfo.lastSanctionTime',
value: userInfo.lastSanctionTime
},
{
localeKey: 'modtools.userinfo.tradingLockCount',
localeKey: 'moderation.userinfo.tradingLockCount',
value: userInfo.tradingLockCount.toString()
},
{
localeKey: 'modtools.userinfo.tradingExpiryDate',
localeKey: 'moderation.userinfo.tradingExpiryDate',
value: userInfo.tradingExpiryDate
},
{
localeKey: 'modtools.userinfo.minutesSinceLastLogin',
localeKey: 'moderation.userinfo.minutesSinceLastLogin',
value: FriendlyTime.format(userInfo.minutesSinceLastLogin * 60, '.ago', 2)
},
{
localeKey: 'modtools.userinfo.lastPurchaseDate',
localeKey: 'moderation.userinfo.lastPurchaseDate',
value: userInfo.lastPurchaseDate
},
{
localeKey: 'modtools.userinfo.primaryEmailAddress',
localeKey: 'moderation.userinfo.primaryEmailAddress',
value: userInfo.primaryEmailAddress
},
{
localeKey: 'modtools.userinfo.identityRelatedBanCount',
localeKey: 'moderation.userinfo.identityRelatedBanCount',
value: userInfo.identityRelatedBanCount.toString()
},
{
localeKey: 'modtools.userinfo.registrationAgeInMinutes',
localeKey: 'moderation.userinfo.registrationAgeInMinutes',
value: FriendlyTime.format(userInfo.registrationAgeInMinutes * 60, '.ago', 2)
},
{
localeKey: 'modtools.userinfo.userClassification',
localeKey: 'moderation.userinfo.userClassification',
value: userInfo.userClassification
}
];
@@ -105,7 +105,7 @@ export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
return (
<>
<NitroCardView className="nitro-mod-tools-user" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ LocalizeText('modtools.userinfo.title', [ 'username' ], [ userInfo.userName ]) } onCloseClick={ () => onCloseClick() } />
<NitroCardHeaderView headerText={ LocalizeText('moderation.userinfo.title', [ 'username' ], [ userInfo.userName ]) } onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black">
<Grid overflow="hidden">
<Column overflow="auto" size={ 8 }>
@@ -130,16 +130,16 @@ export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
</Column>
<Column gap={ 1 } size={ 4 }>
<Button onClick={ event => CreateLinkEvent(`mod-tools/open-user-chatlog/${ userId }`) }>
Room Chat
{ LocalizeText('moderation.userinfo.roomchat') }
</Button>
<Button onClick={ event => setSendMessageVisible(!sendMessageVisible) }>
Send Message
{ LocalizeText('moderation.userinfo.sendmessage') }
</Button>
<Button onClick={ event => setRoomVisitsVisible(!roomVisitsVisible) }>
Room Visits
{ LocalizeText('moderation.userinfo.roomvisits') }
</Button>
<Button onClick={ event => setModActionVisible(!modActionVisible) }>
Mod Action
{ LocalizeText('moderation.userinfo.modaction') }
</Button>
</Column>
</Grid>
@@ -10,6 +10,6 @@ export const GetConfirmLayout = (item: NotificationConfirmItem, onClose: () => v
switch(item.confirmType)
{
default:
return <NotificationDefaultConfirmView { ...props } />;
return <NotificationDefaultConfirmView key={ item.id } item={ item } onClose={ onClose } />;
}
};
@@ -45,7 +45,8 @@ const BadgeMiniPicker: FC<{
return (
<div
ref={ ref }
className="absolute right-[calc(100%+8px)] top-0 z-50 bg-[rgba(28,28,32,0.97)] border border-white/20 rounded-md p-2 shadow-lg min-w-[160px]"
className="absolute right-[calc(100%+8px)] top-0 z-50 border border-white/20 rounded-md p-2 shadow-lg min-w-[160px]"
style={ { backgroundColor: 'var(--ui-dark-bg, rgba(28,28,32,0.97))' } }
onClick={ e => e.stopPropagation() }>
<input
autoFocus
@@ -220,8 +220,8 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
canUse = true;
isCrackable = true;
crackableHits = stuffData.hits;
crackableTarget = stuffData.target;
crackableHits = stuffData?.hits ?? 0;
crackableTarget = stuffData?.target ?? 0;
}
else if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX)
@@ -458,7 +458,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
return (
<Column alignItems="end" gap={ 1 }>
<Column className="relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto bg-[rgba(28,28,32,.95)] [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded">
<Column className="relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded" style={ { backgroundColor: 'var(--ui-dark-bg, rgba(28,28,32,.95))' } }>
<Column className="h-full p-[8px] overflow-auto" gap={ 1 } overflow="visible">
<div className="flex flex-col gap-1">
<Flex alignItems="center" gap={ 1 } justifyContent="between">
@@ -527,7 +527,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
{ isCrackable &&
<>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
<Text small wrap variant="white">{ LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ crackableHits.toString(), crackableTarget.toString() ]) }</Text>
<Text small wrap variant="white">{ LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ (crackableHits ?? 0).toString(), (crackableTarget ?? 0).toString() ]) }</Text>
</> }
{ avatarInfo.groupId > 0 &&
<>
@@ -552,7 +552,21 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
{ godMode &&
<>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
{ canSeeFurniId && <Text small wrap variant="white">ID: { avatarInfo.id }</Text> }
{ canSeeFurniId &&
<div className="flex items-center gap-3">
<div className="flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#7ec8e3]">
<path fillRule="evenodd" d="M4.93 1.31a41.401 41.401 0 0 1 10.14 0C16.194 1.45 17 2.414 17 3.517V18.25a.75.75 0 0 1-1.075.676l-2.8-1.344-2.8 1.344a.75.75 0 0 1-.65 0l-2.8-1.344-2.8 1.344A.75.75 0 0 1 3 18.25V3.517c0-1.103.806-2.068 1.93-2.207Z" clipRule="evenodd" />
</svg>
<Text small wrap variant="white">ID: { avatarInfo.id }</Text>
</div>
<div className="flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#7ec8e3]">
<path d="M5.127 3.502 5.25 3.5h9.5c.041 0 .082 0 .123.002A2.251 2.251 0 0 0 12.75 2h-5.5a2.25 2.25 0 0 0-2.123 1.502ZM1 10.25A2.25 2.25 0 0 1 3.25 8h13.5A2.25 2.25 0 0 1 19 10.25v5.5A2.25 2.25 0 0 1 16.75 18H3.25A2.25 2.25 0 0 1 1 15.75v-5.5ZM3.25 6.5c-.04 0-.082 0-.123.002A2.25 2.25 0 0 1 5.25 5h9.5c.98 0 1.814.627 2.123 1.502a3.819 3.819 0 0 0-.123-.002H3.25Z" />
</svg>
<Text small wrap variant="white">Sprite: { (() => { const ro = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); return ro?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID) ?? '?'; })() }</Text>
</div>
</div> }
{ (!avatarInfo.isWallItem && canMove) &&
<>
<button
@@ -560,6 +574,19 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
onClick={ () => setDropdownOpen(!dropdownOpen) }>
{ dropdownOpen ? `${LocalizeText('widget.furni.present.close')} Buildtools` : `${LocalizeText('navigator.roomsettings.doormode.open')} Buildtools` }
</button>
<button
className="w-full text-white text-xs bg-[#1e7295] hover:bg-[#1a617f] border border-[#ffffff33] rounded px-2 py-1 cursor-pointer transition-colors"
onClick={ () =>
{
const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR);
const typeId = roomObject?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID);
CreateLinkEvent('furni-editor/show');
if(typeId) window.dispatchEvent(new CustomEvent('furni-editor:open', { detail: { spriteId: typeId } }));
} }>
Edit Furni
</button>
{ dropdownOpen &&
<div className="flex gap-[4px] w-full">
{ /* Left panel: position + rotation */ }
@@ -1,4 +1,4 @@
import { GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer';
import { CreateLinkEvent, GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer';
import { Dispatch, FC, FocusEvent, KeyboardEvent, SetStateAction, useCallback, useEffect, useState } from 'react';
import { FaPencilAlt, FaTimes } from 'react-icons/fa';
import { AvatarInfoUser, CloneObject, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api';
@@ -7,7 +7,6 @@ import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks';
import { InfoStandBadgeSlotView } from './InfoStandBadgeSlotView';
import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView';
import { InfoStandWidgetUserTagsView } from './InfoStandWidgetUserTagsView';
import { BackgroundsView } from '../../../../backgrounds/BackgroundsView';
interface InfoStandWidgetUserViewProps {
avatarInfo: AvatarInfoUser;
@@ -32,7 +31,7 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]);
const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); setIsVisible(prev => !prev); }, []);
const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); CreateLinkEvent('interface-settings/profile'); }, []);
const saveMotto = (motto: string) => {
if (!isEditingMotto || motto.length > GetConfigurationValue<number>('motto.max.length', 38) || !roomSession) return;
@@ -127,7 +126,7 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
return (
<>
<Column className="relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto bg-[rgba(28,28,32,0.95)] [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded">
<Column className="relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded" style={ { backgroundColor: 'var(--ui-dark-bg, rgba(28,28,32,0.95))' } }>
<Column className="h-full p-[8px] overflow-auto" gap={1} overflow="visible">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
@@ -257,19 +256,6 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
)}
</Column>
</Column>
{isVisible && avatarInfo.type === AvatarInfoUser.OWN_USER && (
<div className="backgrounds-view-container">
<BackgroundsView
setIsVisible={setIsVisible}
selectedBackground={backgroundId}
setSelectedBackground={setBackgroundId}
selectedStand={standId}
setSelectedStand={setStandId}
selectedOverlay={overlayId}
setSelectedOverlay={setOverlayId}
/>
</div>
)}
</>
);
};
@@ -3,16 +3,21 @@ import { Flex, FlexProps } from '../../../../common';
export const ContextMenuHeaderView: FC<FlexProps> = props =>
{
const { justifyContent = 'center', alignItems = 'center', classNames = [], ...rest } = props;
const { justifyContent = 'center', alignItems = 'center', classNames = [], style = {}, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'bg-[#3d5f6e] text-[#fff] min-w-[117px] h-[25px] max-h-[25px] text-[16px] mb-[2px]', 'p-1' ];
const newClassNames: string[] = [ 'text-[#fff] min-w-[117px] h-[25px] max-h-[25px] text-[16px] mb-[2px]', 'p-1' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return <Flex alignItems={ alignItems } classNames={ getClassNames } justifyContent={ justifyContent } { ...rest } />;
const mergedStyle = useMemo(() => ({
backgroundColor: 'var(--ui-ctx-header-bg, #3d5f6e)',
...style
}), [ style ]);
return <Flex alignItems={ alignItems } classNames={ getClassNames } justifyContent={ justifyContent } style={ mergedStyle } { ...rest } />;
};
@@ -8,7 +8,7 @@ interface ContextMenuListItemViewProps extends FlexProps
export const ContextMenuListItemView: FC<ContextMenuListItemViewProps> = props =>
{
const { disabled = false, fullWidth = true, justifyContent = 'center', alignItems = 'center', classNames = [], onClick = null, ...rest } = props;
const { disabled = false, fullWidth = true, justifyContent = 'center', alignItems = 'center', classNames = [], style = {}, onClick = null, ...rest } = props;
const handleClick = (event: MouseEvent<HTMLDivElement>) =>
{
@@ -19,7 +19,7 @@ export const ContextMenuListItemView: FC<ContextMenuListItemViewProps> = props =
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'relative mb-[2px] p-[3px] overflow-hidden', 'h-[24px] max-h-[24px] p-[3px] bg-[repeating-linear-gradient(#131e25,#131e25_50%,#0d171d_50%,#0d171d_100%)] cursor-pointer' ];
const newClassNames: string[] = [ 'relative mb-[2px] p-[3px] overflow-hidden', 'h-[24px] max-h-[24px] p-[3px] cursor-pointer' ];
if(disabled) newClassNames.push('disabled');
@@ -28,5 +28,10 @@ export const ContextMenuListItemView: FC<ContextMenuListItemViewProps> = props =
return newClassNames;
}, [ disabled, classNames ]);
return <Flex alignItems={ alignItems } classNames={ getClassNames } fullWidth={ fullWidth } justifyContent={ justifyContent } onClick={ handleClick } { ...rest } />;
const mergedStyle = useMemo(() => ({
background: 'repeating-linear-gradient(var(--ui-ctx-item-bg1, #131e25), var(--ui-ctx-item-bg1, #131e25) 50%, var(--ui-ctx-item-bg2, #0d171d) 50%, var(--ui-ctx-item-bg2, #0d171d) 100%)',
...style
}), [ style ]);
return <Flex alignItems={ alignItems } classNames={ getClassNames } fullWidth={ fullWidth } justifyContent={ justifyContent } onClick={ handleClick } style={ mergedStyle } { ...rest } />;
};
@@ -76,7 +76,6 @@ export const ContextMenuView: FC<ContextMenuViewProps> = ({
const getClassNames = useMemo(() => {
const classes = [
'p-[2px]!',
'bg-[#1c323f]',
'border-2',
'border-[solid]',
'border-[rgba(255,255,255,.5)]',
@@ -98,6 +97,7 @@ export const ContextMenuView: FC<ContextMenuViewProps> = ({
top: pos.y ?? 0,
transition: isFading ? 'opacity 75ms linear' : undefined,
opacity,
backgroundColor: 'var(--ui-ctx-bg, #1c323f)',
...style,
}),
[pos, opacity, isFading, style]
+30 -30
View File
@@ -69,38 +69,38 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
<ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } />
</motion.div> )}
</AnimatePresence>
<Flex alignItems="center" className="absolute bottom-0 left-0 w-full h-[55px] bg-[rgba(28,28,32,.95)] [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] py-1 px-3" gap={ 2 } justifyContent="between">
<Flex alignItems="center" gap={ 2 }>
<Flex alignItems="center" gap={ 2 }>
<Flex center pointer className={ 'relative w-[50px] h-[45px] overflow-hidden ' + (isMeExpanded ? 'active ' : '') } onClick={ event =>
{
setMeExpanded(!isMeExpanded);
event.stopPropagation();
} }>
<LayoutAvatarImageView className="-ml-[5px] mt-[25px]" direction={ 2 } figure={ userFigure } position="absolute" />
{ (getTotalUnseen > 0) &&
<LayoutItemCountView count={ getTotalUnseen } /> }
</Flex>
{ isInRoom &&
<ToolbarItemView icon="habbo" onClick={ event => VisitDesktop() } /> }
{ !isInRoom &&
<ToolbarItemView icon="house" onClick={ event => CreateLinkEvent('navigator/goto/home') } /> }
<ToolbarItemView icon="rooms" onClick={ event => CreateLinkEvent('navigator/toggle') } />
{ GetConfigurationValue('game.center.enabled') &&
<ToolbarItemView icon="game" onClick={ event => CreateLinkEvent('games/toggle') } /> }
<ToolbarItemView icon="catalog" onClick={ event => CreateLinkEvent('catalog/toggle') } />
<ToolbarItemView icon="inventory" onClick={ event => CreateLinkEvent('inventory/toggle') }>
{ (getFullCount > 0) &&
<LayoutItemCountView count={ getFullCount } /> }
</ToolbarItemView>
{ isInRoom &&
<ToolbarItemView icon="camera" onClick={ event => CreateLinkEvent('camera/toggle') } /> }
{ isMod &&
<ToolbarItemView icon="modtools" onClick={ event => CreateLinkEvent('mod-tools/toggle') } /> }
<Flex alignItems="center" className="absolute bottom-0 left-0 w-full h-[55px] [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] py-1 px-3" gap={ 2 } style={ { backgroundColor: 'var(--ui-dark-bg, rgba(28,28,32,.95))' } }>
<Flex alignItems="center" gap={ 2 } className="flex-shrink-0">
<Flex center pointer className={ 'relative w-[50px] h-[45px] overflow-hidden ' + (isMeExpanded ? 'active ' : '') } onClick={ event =>
{
setMeExpanded(!isMeExpanded);
event.stopPropagation();
} }>
<LayoutAvatarImageView className="-ml-[5px] mt-[25px]" direction={ 2 } figure={ userFigure } position="absolute" />
{ (getTotalUnseen > 0) &&
<LayoutItemCountView count={ getTotalUnseen } /> }
</Flex>
<Flex alignItems="center" id="toolbar-chat-input-container" />
{ isInRoom &&
<ToolbarItemView icon="habbo" onClick={ event => VisitDesktop() } /> }
{ !isInRoom &&
<ToolbarItemView icon="house" onClick={ event => CreateLinkEvent('navigator/goto/home') } /> }
<ToolbarItemView icon="rooms" onClick={ event => CreateLinkEvent('navigator/toggle') } />
{ GetConfigurationValue('game.center.enabled') &&
<ToolbarItemView icon="game" onClick={ event => CreateLinkEvent('games/toggle') } /> }
<ToolbarItemView icon="catalog" onClick={ event => CreateLinkEvent('catalog/toggle') } />
<ToolbarItemView icon="inventory" onClick={ event => CreateLinkEvent('inventory/toggle') }>
{ (getFullCount > 0) &&
<LayoutItemCountView count={ getFullCount } /> }
</ToolbarItemView>
{ isInRoom &&
<ToolbarItemView icon="camera" onClick={ event => CreateLinkEvent('camera/toggle') } /> }
{ isMod &&
<ToolbarItemView icon="modtools" onClick={ event => CreateLinkEvent('mod-tools/toggle') } /> }
{ isMod &&
<ToolbarItemView icon="catalog" onClick={ event => CreateLinkEvent('furni-editor/toggle') } /> }
</Flex>
<Flex alignItems="center" gap={ 2 }>
<Flex alignItems="center" justifyContent="center" className="flex-1 min-w-0 max-w-[600px] mx-auto" id="toolbar-chat-input-container" />
<Flex alignItems="center" gap={ 2 } className="flex-shrink-0">
<Flex gap={ 2 }>
<ToolbarItemView icon="friendall" onClick={ event => CreateLinkEvent('friends/toggle') }>
{ (requests.length > 0) &&
@@ -7,6 +7,131 @@ import { NitroInput } from '../../../../layout';
import { WiredActionBaseView } from './WiredActionBaseView';
import { WiredSourcesSelector } from '../WiredSourcesSelector';
type RewardType = 'badge' | 'credits' | 'pixels' | 'diamonds' | 'points' | 'furni' | 'respect';
interface RewardEntry
{
rewardType: RewardType;
rewardValue: string;
probability: number;
pointsType: number;
}
const DEFAULT_PROBABILITY = 100;
const DEFAULT_POINTS_TYPE = 5;
const REWARD_TYPES: { value: RewardType, label: string }[] = [
{ value: 'badge', label: 'Badge' },
{ value: 'credits', label: 'Credits' },
{ value: 'pixels', label: 'Pixels / Duckets' },
{ value: 'diamonds', label: 'Diamonds' },
{ value: 'points', label: 'Extra Currency' },
{ value: 'furni', label: 'Furni' },
{ value: 'respect', label: 'Respect' }
];
const SELECTABLE_REWARD_TYPES = REWARD_TYPES.filter(entry => (entry.value !== 'respect'));
const createReward = (): RewardEntry =>
({
rewardType: 'furni',
rewardValue: '',
probability: DEFAULT_PROBABILITY,
pointsType: DEFAULT_POINTS_TYPE
});
const getRewardValuePlaceholder = (rewardType: RewardType) =>
{
switch(rewardType)
{
case 'badge':
return 'Badge code';
case 'credits':
return 'Credits amount';
case 'pixels':
return 'Pixels amount';
case 'diamonds':
return 'Diamonds amount';
case 'points':
return 'Amount';
case 'furni':
return 'Furni base item id';
case 'respect':
return 'Respect amount';
}
};
const getExtraFieldLabel = (rewardType: RewardType) =>
{
switch(rewardType)
{
case 'points':
return 'Currency Type';
case 'badge':
return 'Code';
default:
return 'Info';
}
};
const getExtraFieldPlaceholder = (rewardType: RewardType) =>
{
switch(rewardType)
{
case 'points':
return 'Type id (e.g. 105)';
case 'badge':
return 'Badge';
default:
return '';
}
};
const parseRewardEntry = (rawType: string, rawCode: string, rawProbability: string): RewardEntry =>
{
const probability = Number(rawProbability);
const parsedProbability = Number.isFinite(probability) ? probability : DEFAULT_PROBABILITY;
if(rawType === '0')
{
return { rewardType: 'badge', rewardValue: rawCode, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE };
}
const separatorIndex = rawCode.indexOf('#');
if(separatorIndex === -1)
{
return { rewardType: 'furni', rewardValue: rawCode, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE };
}
const rewardType = rawCode.slice(0, separatorIndex);
const rewardValue = rawCode.slice(separatorIndex + 1);
if(rewardType.startsWith('points'))
{
const pointsType = Number(rewardType.slice('points'.length));
return {
rewardType: 'points',
rewardValue,
probability: parsedProbability,
pointsType: Number.isFinite(pointsType) ? pointsType : DEFAULT_POINTS_TYPE
};
}
if(REWARD_TYPES.some(entry => (entry.value === rewardType)))
{
return { rewardType: rewardType as RewardType, rewardValue, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE };
}
if(rewardType === 'cata')
{
return { rewardType: 'furni', rewardValue, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE };
}
return { rewardType: 'furni', rewardValue: rawCode, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE };
};
export const WiredActionGiveRewardView: FC<{}> = props =>
{
const [ limitEnabled, setLimitEnabled ] = useState(false);
@@ -14,7 +139,7 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
const [ uniqueRewards, setUniqueRewards ] = useState(false);
const [ rewardsLimit, setRewardsLimit ] = useState(1);
const [ limitationInterval, setLimitationInterval ] = useState(1);
const [ rewards, setRewards ] = useState<{ isBadge: boolean, itemCode: string, probability: number }[]>([]);
const [ rewards, setRewards ] = useState<RewardEntry[]>([]);
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
const [ userSource, setUserSource ] = useState<number>(() =>
{
@@ -22,7 +147,8 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
return 0;
});
const addReward = () => setRewards(rewards => [ ...rewards, { isBadge: false, itemCode: '', probability: null } ]);
const addReward = () => setRewards(rewards => [ ...rewards, createReward() ]);
const hasCustomCurrencyReward = rewards.some(reward => (reward.rewardType === 'points'));
const removeReward = (index: number) =>
{
@@ -36,18 +162,9 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
});
};
const updateReward = (index: number, isBadge: boolean, itemCode: string, probability: number) =>
const updateReward = (index: number, updater: (reward: RewardEntry) => RewardEntry) =>
{
const rewardsClone = Array.from(rewards);
const reward = rewardsClone[index];
if(!reward) return;
reward.isBadge = isBadge;
reward.itemCode = itemCode;
reward.probability = probability;
setRewards(rewardsClone);
setRewards(prevValue => prevValue.map((reward, rewardIndex) => ((rewardIndex === index) ? updater(reward) : reward)));
};
const save = () =>
@@ -56,9 +173,20 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
for(const reward of rewards)
{
if(!reward.itemCode) continue;
const rewardValue = reward.rewardValue.trim();
const rewardsString = [ reward.isBadge ? '0' : '1', reward.itemCode, reward.probability.toString() ];
if(!rewardValue) continue;
const probability = Math.max(0, Number.isFinite(reward.probability) ? reward.probability : DEFAULT_PROBABILITY);
const rewardCode = (() =>
{
if(reward.rewardType === 'badge') return rewardValue;
if(reward.rewardType === 'points') return `points${ Math.max(0, reward.pointsType) }#${ rewardValue }`;
return `${ reward.rewardType }#${ rewardValue }`;
})();
const rewardsString = [ reward.rewardType === 'badge' ? '0' : '1', rewardCode, (uniqueRewards ? DEFAULT_PROBABILITY : probability).toString() ];
stringRewards.push(rewardsString.join(','));
}
@@ -71,9 +199,9 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
useEffect(() =>
{
const readRewards: { isBadge: boolean, itemCode: string, probability: number }[] = [];
const readRewards: RewardEntry[] = [];
if(trigger.stringData.length > 0 && trigger.stringData.includes(';'))
if(trigger.stringData.length > 0)
{
const splittedRewards = trigger.stringData.split(';');
@@ -83,11 +211,11 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
if(reward.length !== 3) continue;
readRewards.push({ isBadge: reward[0] === '0', itemCode: reward[1], probability: Number(reward[2]) });
readRewards.push(parseRewardEntry(reward[0], reward[1], reward[2]));
}
}
if(readRewards.length === 0) readRewards.push({ isBadge: false, itemCode: '', probability: null });
if(readRewards.length === 0) readRewards.push(createReward());
setRewardTime((trigger.intData.length > 0) ? trigger.intData[0] : 0);
setUniqueRewards((trigger.intData.length > 1) ? (trigger.intData[1] === 1) : false);
@@ -147,24 +275,64 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
</Button>
</div>
<div className="flex flex-col gap-1">
<div className="grid grid-cols-[1.2fr_1fr_110px_150px_42px] gap-1 px-1">
<Text small bold>Type</Text>
<Text small bold>Amount / Value</Text>
<Text small bold>{ uniqueRewards ? 'Mode' : 'Chance %' }</Text>
<Text small bold>{ hasCustomCurrencyReward ? 'Currency Type' : 'Extra / Info' }</Text>
<Text small bold>Action</Text>
</div>
{ rewards && rewards.map((reward, index) =>
{
const rewardTypeOptions = (reward.rewardType === 'respect')
? REWARD_TYPES
: SELECTABLE_REWARD_TYPES;
return (
<div key={ index } className="flex gap-1">
<div className="flex items-center gap-1">
<input checked={ reward.isBadge } className="form-check-input" type="checkbox" onChange={ (e) => updateReward(index, e.target.checked, reward.itemCode, reward.probability) } />
<Text small>Badge?</Text>
<div key={ index } className="grid grid-cols-[1.2fr_1fr_110px_150px_42px] gap-1">
<select className="w-full form-select form-select-sm" value={ reward.rewardType } onChange={ event => updateReward(index, prevValue => ({ ...prevValue, rewardType: event.target.value as RewardType, rewardValue: '' })) }>
{ rewardTypeOptions.map(entry => <option key={ entry.value } value={ entry.value }>{ entry.label }</option>) }
</select>
<NitroInput
placeholder={ getRewardValuePlaceholder(reward.rewardType) }
type={ reward.rewardType === 'badge' ? 'text' : 'number' }
value={ reward.rewardValue }
onChange={ event => updateReward(index, prevValue => ({ ...prevValue, rewardValue: event.target.value })) } />
{ uniqueRewards
? <div className="flex items-center px-2 rounded bg-muted">
<Text small>Unique</Text>
</div>
: <NitroInput
min={ 0 }
max={ 100 }
placeholder="Chance %"
type="number"
value={ reward.probability }
onChange={ event => updateReward(index, prevValue => ({ ...prevValue, probability: Number(event.target.value) })) } /> }
{ (reward.rewardType === 'points')
?
<NitroInput
min={ 0 }
placeholder={ getExtraFieldPlaceholder(reward.rewardType) }
type="number"
value={ reward.pointsType }
onChange={ event => updateReward(index, prevValue => ({ ...prevValue, pointsType: Number(event.target.value) })) } />
: <div className="flex items-center px-2 rounded bg-muted">
<Text small>{ getExtraFieldLabel(reward.rewardType) }</Text>
</div> }
<div className="flex items-center justify-end">
{ (index > 0) &&
<Button variant="danger" onClick={ event => removeReward(index) }>
<FaTrash className="fa-icon" />
</Button> }
</div>
<NitroInput placeholder="Item Code" type="text" value={ reward.itemCode } onChange={ e => updateReward(index, reward.isBadge, e.target.value, reward.probability) } />
<NitroInput placeholder="Probability" type="number" value={ reward.probability } onChange={ e => updateReward(index, reward.isBadge, reward.itemCode, Number(e.target.value)) } />
{ (index > 0) &&
<Button variant="danger" onClick={ event => removeReward(index) }>
<FaTrash className="fa-icon" />
</Button> }
</div>
);
}) }
</div>
<Text center small className="p-1 rounded bg-muted">
Extra Currency uses Amount as the quantity and Currency Type as the purse type id. Example: amount 200 + type 105.
</Text>
</WiredActionBaseView>
);
};
@@ -28,6 +28,7 @@ import { WiredActionMoveAndRotateFurniView } from './WiredActionMoveAndRotateFur
import { WiredActionMoveFurniToView } from './WiredActionMoveFurniToView';
import { WiredActionMoveFurniView } from './WiredActionMoveFurniView';
import { WiredActionMuteUserView } from './WiredActionMuteUserView';
import { WiredActionRelativeMoveView } from './WiredActionRelativeMoveView';
import { WiredActionResetView } from './WiredActionResetView';
import { WiredActionSetFurniStateToView } from './WiredActionSetFurniStateToView';
import { WiredActionTeleportView } from './WiredActionTeleportView';
@@ -86,6 +87,8 @@ export const WiredActionLayoutView = (code: number) =>
return <WiredActionMoveFurniToView />;
case WiredActionLayoutCode.MUTE_USER:
return <WiredActionMuteUserView />;
case WiredActionLayoutCode.RELATIVE_MOVE:
return <WiredActionRelativeMoveView />;
case WiredActionLayoutCode.RESET:
return <WiredActionResetView />;
case WiredActionLayoutCode.SET_FURNI_STATE:
@@ -0,0 +1,120 @@
import { FC, useEffect, useState } from 'react';
import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp } from 'react-icons/fa';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Slider, Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredSourcesSelector } from '../WiredSourcesSelector';
import { WiredActionBaseView } from './WiredActionBaseView';
const MAX_DISTANCE = 20;
const HORIZONTAL_OPTIONS = [
{ value: 0, icon: <FaArrowLeft /> },
{ value: 1, icon: <FaArrowRight /> }
];
const VERTICAL_OPTIONS = [
{ value: 0, icon: <FaArrowDown /> },
{ value: 1, icon: <FaArrowUp /> }
];
const normalizeDirection = (value: number, fallback = 1) =>
{
if(value === 0 || value === 1) return value;
return fallback;
};
const normalizeDistance = (value: number) =>
{
if(isNaN(value)) return 0;
return Math.max(0, Math.min(MAX_DISTANCE, value));
};
export const WiredActionRelativeMoveView: FC<{}> = () =>
{
const { trigger = null, setIntParams = null } = useWired();
const [horizontalDirection, setHorizontalDirection] = useState(1);
const [horizontalDistance, setHorizontalDistance] = useState(0);
const [verticalDirection, setVerticalDirection] = useState(1);
const [verticalDistance, setVerticalDistance] = useState(0);
const [ furniSource, setFurniSource ] = useState<number>(() =>
{
if(trigger?.intData?.length > 4) return trigger.intData[4];
return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0;
});
useEffect(() =>
{
if(!trigger) return;
setHorizontalDirection((trigger.intData.length > 0) ? normalizeDirection(trigger.intData[0], 1) : 1);
setHorizontalDistance((trigger.intData.length > 1) ? normalizeDistance(trigger.intData[1]) : 0);
setVerticalDirection((trigger.intData.length > 2) ? normalizeDirection(trigger.intData[2], 1) : 1);
setVerticalDistance((trigger.intData.length > 3) ? normalizeDistance(trigger.intData[3]) : 0);
if(trigger.intData.length > 4) setFurniSource(trigger.intData[4]);
else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0);
}, [ trigger ]);
const save = () => setIntParams([
horizontalDirection,
horizontalDistance,
verticalDirection,
verticalDistance,
furniSource
]);
return (
<WiredActionBaseView
hasSpecialInput={ true }
requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT }
save={ save }
footer={ <WiredSourcesSelector showFurni={ true } furniSource={ furniSource } onChangeFurni={ setFurniSource } /> }>
<div className="flex flex-col gap-2">
<Text bold>{ LocalizeText('wiredfurni.params.movement.horizontal.selection') }</Text>
<div className="flex gap-2">
{ HORIZONTAL_OPTIONS.map(option =>
{
return (
<label key={ option.value } className="flex items-center gap-1">
<input checked={ (horizontalDirection === option.value) } className="form-check-input" name="relativeMoveHorizontal" type="radio" onChange={ () => setHorizontalDirection(option.value) } />
<Text>{ option.icon }</Text>
</label>
);
}) }
</div>
<Text>{ LocalizeText('wiredfurni.params.movement.horizontal.distance', [ 'distance' ], [ horizontalDistance.toString() ]) }</Text>
<Slider
max={ MAX_DISTANCE }
min={ 0 }
step={ 1 }
value={ horizontalDistance }
onChange={ value => setHorizontalDistance(value as number) } />
</div>
<div className="flex flex-col gap-2">
<Text bold>{ LocalizeText('wiredfurni.params.movement.vertical.selection') }</Text>
<div className="flex gap-2">
{ VERTICAL_OPTIONS.map(option =>
{
return (
<label key={ option.value } className="flex items-center gap-1">
<input checked={ (verticalDirection === option.value) } className="form-check-input" name="relativeMoveVertical" type="radio" onChange={ () => setVerticalDirection(option.value) } />
<Text>{ option.icon }</Text>
</label>
);
}) }
</div>
<Text>{ LocalizeText('wiredfurni.params.movement.vertical.distance', [ 'distance' ], [ verticalDistance.toString() ]) }</Text>
<Slider
max={ MAX_DISTANCE }
min={ 0 }
step={ 1 }
value={ verticalDistance }
onChange={ value => setVerticalDistance(value as number) } />
</div>
</WiredActionBaseView>
);
};
@@ -0,0 +1,162 @@
import { FC, useEffect, useMemo, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Slider, Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredSourcesSelector } from '../WiredSourcesSelector';
import { WiredActionBaseView } from './WiredActionBaseView';
const MIN_ALTITUDE = 0;
const MAX_ALTITUDE = 40;
const ALTITUDE_STEP = 0.01;
const ALTITUDE_PATTERN = /^\d*(\.\d{0,2})?$/;
const clampAltitude = (value: number) =>
{
if(isNaN(value)) return MIN_ALTITUDE;
const clamped = Math.min(MAX_ALTITUDE, Math.max(MIN_ALTITUDE, value));
return parseFloat(clamped.toFixed(2));
};
const formatAltitude = (value: number) =>
{
const normalized = clampAltitude(value);
const text = normalized.toFixed(2);
return text.replace(/\.00$/, '').replace(/(\.\d)0$/, '$1');
};
const parseAltitude = (value: string) =>
{
if(!value || !value.trim().length) return 0;
const parsed = parseFloat(value);
if(isNaN(parsed)) return 0;
return clampAltitude(parsed);
};
const OPERATOR_OPTIONS = [
{ value: 0, label: 'wiredfurni.params.operator.0' },
{ value: 1, label: 'wiredfurni.params.operator.1' },
{ value: 2, label: 'wiredfurni.params.operator.2' }
];
const normalizeOperator = (value: number) =>
{
if(value < 0 || value > 2) return 2;
return value;
};
export const WiredActionSetAltitudeView: FC<{}> = () =>
{
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
const [ operator, setOperator ] = useState(2);
const [ furniSource, setFurniSource ] = useState<number>(() =>
{
if(trigger?.intData?.length > 1) return trigger.intData[1];
return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0;
});
const [ altitude, setAltitude ] = useState(0);
const [ altitudeInput, setAltitudeInput ] = useState('0');
const normalizedAltitudeText = useMemo(() => formatAltitude(altitude), [ altitude ]);
useEffect(() =>
{
if(!trigger) return;
setOperator((trigger.intData.length > 0) ? normalizeOperator(trigger.intData[0]) : 2);
setFurniSource((trigger.intData.length > 1) ? trigger.intData[1] : ((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0));
const nextAltitude = parseAltitude(trigger.stringData);
setAltitude(nextAltitude);
setAltitudeInput(formatAltitude(nextAltitude));
}, [ trigger ]);
const updateAltitude = (value: number) =>
{
const nextValue = clampAltitude(value);
setAltitude(nextValue);
setAltitudeInput(formatAltitude(nextValue));
};
const updateAltitudeInput = (value: string) =>
{
if(!ALTITUDE_PATTERN.test(value)) return;
setAltitudeInput(value);
if(!value.length)
{
setAltitude(0);
return;
}
const parsedValue = parseFloat(value);
if(isNaN(parsedValue)) return;
if(parsedValue > MAX_ALTITUDE)
{
updateAltitude(MAX_ALTITUDE);
return;
}
setAltitude(clampAltitude(parsedValue));
};
const save = () =>
{
setIntParams([
operator,
furniSource
]);
setStringParam(normalizedAltitudeText);
};
return (
<WiredActionBaseView
hasSpecialInput={ true }
requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT }
save={ save }
footer={ <WiredSourcesSelector showFurni={ true } furniSource={ furniSource } onChangeFurni={ setFurniSource } /> }>
<div className="flex flex-col gap-2">
{ OPERATOR_OPTIONS.map(option =>
{
return (
<div key={ option.value } className="flex items-center gap-1">
<input checked={ (operator === option.value) } className="form-check-input" id={ `setAltitudeOperator${ option.value }` } name="setAltitudeOperator" type="radio" onChange={ () => setOperator(option.value) } />
<Text>{ LocalizeText(option.label) }</Text>
</div>
);
}) }
</div>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.setaltitude') }</Text>
<input
className="form-control form-control-sm"
inputMode="decimal"
type="text"
value={ altitudeInput }
onBlur={ () => setAltitudeInput(formatAltitude(altitude)) }
onChange={ event => updateAltitudeInput(event.target.value) } />
</div>
<div className="flex flex-col gap-1">
<Slider
max={ MAX_ALTITUDE }
min={ MIN_ALTITUDE }
step={ ALTITUDE_STEP }
value={ altitude }
onChange={ event => updateAltitude(event as number) } />
<Text small>{ normalizedAltitudeText }</Text>
</div>
</WiredActionBaseView>
);
};
@@ -0,0 +1,39 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { NitroInput } from '../../../../layout';
import { WiredTriggerBaseView } from './WiredTriggerBaseView';
export const WiredTriggerAvatarLeaveRoomView: FC<{}> = props =>
{
const [ username, setUsername ] = useState('');
const [ avatarMode, setAvatarMode ] = useState(0);
const { trigger = null, setStringParam = null } = useWired();
const save = () => setStringParam((avatarMode === 1) ? username : '');
useEffect(() =>
{
setUsername(trigger.stringData);
setAvatarMode(trigger.stringData ? 1 : 0);
}, [ trigger ]);
return (
<WiredTriggerBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save }>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.picktriggerer') }</Text>
<div className="flex items-center gap-1">
<input checked={ (avatarMode === 0) } className="form-check-input" id="avatarMode0" name="avatarMode" type="radio" onChange={ event => setAvatarMode(0) } />
<Text>{ LocalizeText('wiredfurni.params.anyavatar') }</Text>
</div>
<div className="flex items-center gap-1">
<input checked={ (avatarMode === 1) } className="form-check-input" id="avatarMode1" name="avatarMode" type="radio" onChange={ event => setAvatarMode(1) } />
<Text>{ LocalizeText('wiredfurni.params.certainavatar') }</Text>
</div>
{ (avatarMode === 1) &&
<NitroInput type="text" value={ username } onChange={ event => setUsername(event.target.value) } /> }
</div>
</WiredTriggerBaseView>
);
};
@@ -0,0 +1,8 @@
import { FC } from 'react';
import { WiredFurniType } from '../../../../api';
import { WiredTriggerBaseView } from './WiredTriggerBaseView';
export const WiredTriggerClickFurniView: FC<{}> = () =>
{
return <WiredTriggerBaseView hasSpecialInput={ false } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_OR_BY_TYPE } save={ null } />;
};
@@ -0,0 +1,20 @@
import { FC, useEffect } from 'react';
import { WiredFurniType } from '../../../../api';
import { useWired } from '../../../../hooks';
import { WiredTriggerBaseView } from './WiredTriggerBaseView';
const CLICK_TILE_INTERACTION_TYPES = [ 'room_invisible_click_tile' ];
export const WiredTriggerClickTileView: FC<{}> = () =>
{
const { setAllowedInteractionTypes } = useWired();
useEffect(() =>
{
setAllowedInteractionTypes(CLICK_TILE_INTERACTION_TYPES);
return () => setAllowedInteractionTypes(null);
}, [ setAllowedInteractionTypes ]);
return <WiredTriggerBaseView hasSpecialInput={ false } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_OR_BY_TYPE } save={ null } />;
};
@@ -0,0 +1,8 @@
import { FC } from 'react';
import { WiredFurniType } from '../../../../api';
import { WiredTriggerBaseView } from './WiredTriggerBaseView';
export const WiredTriggerClickUserView: FC<{}> = () =>
{
return <WiredTriggerBaseView hasSpecialInput={ false } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ null } />;
};
@@ -0,0 +1,32 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Slider, Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredTriggerBaseView } from './WiredTriggerBaseView';
export const WiredTriggeExecutePeriodicallyShortView: FC<{}> = () =>
{
const [ time, setTime ] = useState(10);
const { trigger = null, setIntParams = null } = useWired();
const save = () => setIntParams([ time ]);
useEffect(() =>
{
setTime((trigger.intData.length > 0) ? trigger.intData[0] : 10);
}, [ trigger ]);
return (
<WiredTriggerBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save }>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.settime', [ 'seconds' ], [ ((time * 50) / 1000).toFixed(2) ]) }</Text>
<Text small>{ `${ time * 50 } ms` }</Text>
<Slider
max={ 10 }
min={ 1 }
value={ time }
onChange={ event => setTime(event) } />
</div>
</WiredTriggerBaseView>
);
};
@@ -1,14 +1,20 @@
import { WiredTriggerLayout } from '../../../../api';
import { WiredTriggerAvatarEnterRoomView } from './WiredTriggerAvatarEnterRoomView';
import { WiredTriggerAvatarLeaveRoomView } from './WiredTriggerAvatarLeaveRoomView';
import { WiredTriggerAvatarSaysSomethingView } from './WiredTriggerAvatarSaysSomethingView';
import { WiredTriggerAvatarWalksOffFurniView } from './WiredTriggerAvatarWalksOffFurniView';
import { WiredTriggerAvatarWalksOnFurniView } from './WiredTriggerAvatarWalksOnFurni';
import { WiredTriggerBotReachedAvatarView } from './WiredTriggerBotReachedAvatarView';
import { WiredTriggerBotReachedStuffView } from './WiredTriggerBotReachedStuffView';
import { WiredTriggerClickFurniView } from './WiredTriggerClickFurniView';
import { WiredTriggerClickTileView } from './WiredTriggerClickTileView';
import { WiredTriggerClickUserView } from './WiredTriggerClickUserView';
import { WiredTriggerCollisionView } from './WiredTriggerCollisionView';
import { WiredTriggerUserPerformsActionView } from './WiredTriggerUserPerformsActionView';
import { WiredTriggeExecuteOnceView } from './WiredTriggerExecuteOnceView';
import { WiredTriggeExecutePeriodicallyLongView } from './WiredTriggerExecutePeriodicallyLongView';
import { WiredTriggeExecutePeriodicallyView } from './WiredTriggerExecutePeriodicallyView';
import { WiredTriggeExecutePeriodicallyShortView } from './WiredTriggerExecutePeriodicallyShortView';
import { WiredTriggerGameEndsView } from './WiredTriggerGameEndsView';
import { WiredTriggerGameStartsView } from './WiredTriggerGameStartsView';
import { WiredTriggeScoreAchievedView } from './WiredTriggerScoreAchievedView';
@@ -21,6 +27,8 @@ export const WiredTriggerLayoutView = (code: number) =>
{
case WiredTriggerLayout.AVATAR_ENTERS_ROOM:
return <WiredTriggerAvatarEnterRoomView />;
case WiredTriggerLayout.AVATAR_LEAVES_ROOM:
return <WiredTriggerAvatarLeaveRoomView />;
case WiredTriggerLayout.AVATAR_SAYS_SOMETHING:
return <WiredTriggerAvatarSaysSomethingView />;
case WiredTriggerLayout.AVATAR_WALKS_OFF_FURNI:
@@ -31,12 +39,22 @@ export const WiredTriggerLayoutView = (code: number) =>
return <WiredTriggerBotReachedAvatarView />;
case WiredTriggerLayout.BOT_REACHED_STUFF:
return <WiredTriggerBotReachedStuffView />;
case WiredTriggerLayout.CLICK_FURNI:
return <WiredTriggerClickFurniView />;
case WiredTriggerLayout.CLICK_TILE:
return <WiredTriggerClickTileView />;
case WiredTriggerLayout.CLICK_USER:
return <WiredTriggerClickUserView />;
case WiredTriggerLayout.USER_PERFORMS_ACTION:
return <WiredTriggerUserPerformsActionView />;
case WiredTriggerLayout.COLLISION:
return <WiredTriggerCollisionView />;
case WiredTriggerLayout.EXECUTE_ONCE:
return <WiredTriggeExecuteOnceView />;
case WiredTriggerLayout.EXECUTE_PERIODICALLY:
return <WiredTriggeExecutePeriodicallyView />;
case WiredTriggerLayout.EXECUTE_PERIODICALLY_SHORT:
return <WiredTriggeExecutePeriodicallyShortView />;
case WiredTriggerLayout.EXECUTE_PERIODICALLY_LONG:
return <WiredTriggeExecutePeriodicallyLongView />;
case WiredTriggerLayout.GAME_ENDS:
@@ -1,8 +1,34 @@
import { FC } from 'react';
import { WiredFurniType } from '../../../../api';
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredTriggerBaseView } from './WiredTriggerBaseView';
export const WiredTriggerToggleFurniView: FC<{}> = props =>
export const WiredTriggerToggleFurniView: FC<{}> = () =>
{
return <WiredTriggerBaseView hasSpecialInput={ false } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_OR_BY_TYPE } save={ null } />;
const [ triggerMode, setTriggerMode ] = useState(0);
const { trigger = null, setIntParams = null } = useWired();
const save = () => setIntParams([ triggerMode ]);
useEffect(() =>
{
setTriggerMode((trigger?.intData?.length > 0) ? trigger.intData[0] : 0);
}, [ trigger ]);
return (
<WiredTriggerBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID } save={ save }>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.condition.state') }</Text>
<div className="flex items-center gap-1">
<input checked={ (triggerMode === 1) } className="form-check-input" id="stateTrigger1" name="stateTrigger" type="radio" onChange={ () => setTriggerMode(1) } />
<Text>{ LocalizeText('wiredfurni.params.state_trigger.1') }</Text>
</div>
<div className="flex items-center gap-1">
<input checked={ (triggerMode === 0) } className="form-check-input" id="stateTrigger0" name="stateTrigger" type="radio" onChange={ () => setTriggerMode(0) } />
<Text>{ LocalizeText('wiredfurni.params.state_trigger.0') }</Text>
</div>
</div>
</WiredTriggerBaseView>
);
};
@@ -0,0 +1,103 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredTriggerBaseView } from './WiredTriggerBaseView';
const ACTION_WAVE = 1;
const ACTION_BLOW_KISS = 2;
const ACTION_LAUGH = 3;
const ACTION_AWAKE = 4;
const ACTION_RELAX = 5;
const ACTION_SIT = 6;
const ACTION_STAND = 7;
const ACTION_LAY = 8;
const ACTION_SIGN = 9;
const ACTION_DANCE = 10;
const ACTION_THUMB_UP = 11;
const ACTION_OPTIONS = [
{ value: ACTION_WAVE, label: 'widget.memenu.wave' },
{ value: ACTION_BLOW_KISS, label: 'widget.memenu.blow' },
{ value: ACTION_LAUGH, label: 'widget.memenu.laugh' },
{ value: ACTION_THUMB_UP, label: 'widget.memenu.thumb' },
{ value: ACTION_AWAKE, label: 'wiredfurni.params.action.4' },
{ value: ACTION_RELAX, label: 'avatar.widget.random_walk' },
{ value: ACTION_SIT, label: 'widget.memenu.sit' },
{ value: ACTION_STAND, label: 'widget.memenu.stand' },
{ value: ACTION_LAY, label: 'wiredfurni.params.action.8' },
{ value: ACTION_SIGN, label: 'widget.memenu.sign' },
{ value: ACTION_DANCE, label: 'widget.memenu.dance' }
];
const SIGN_OPTIONS = Array.from({ length: 18 }, (_, value) => ({
value,
label: `wiredfurni.params.action.sign.${ value }`
}));
const DANCE_OPTIONS = [
{ value: 1, label: 'widget.memenu.dance1' },
{ value: 2, label: 'widget.memenu.dance2' },
{ value: 3, label: 'widget.memenu.dance3' },
{ value: 4, label: 'widget.memenu.dance4' }
];
export const WiredTriggerUserPerformsActionView: FC<{}> = () =>
{
const [ selectedAction, setSelectedAction ] = useState(ACTION_WAVE);
const [ signFilterEnabled, setSignFilterEnabled ] = useState(false);
const [ signId, setSignId ] = useState(0);
const [ danceFilterEnabled, setDanceFilterEnabled ] = useState(false);
const [ danceId, setDanceId ] = useState(1);
const { trigger = null, setIntParams = null } = useWired();
const save = () => setIntParams([
selectedAction,
signFilterEnabled ? 1 : 0,
signId,
danceFilterEnabled ? 1 : 0,
danceId
]);
useEffect(() =>
{
setSelectedAction((trigger?.intData?.length > 0) ? trigger.intData[0] : ACTION_WAVE);
setSignFilterEnabled((trigger?.intData?.length > 1) ? (trigger.intData[1] === 1) : false);
setSignId((trigger?.intData?.length > 2) ? trigger.intData[2] : 0);
setDanceFilterEnabled((trigger?.intData?.length > 3) ? (trigger.intData[3] === 1) : false);
setDanceId((trigger?.intData?.length > 4) ? trigger.intData[4] : 1);
}, [ trigger ]);
return (
<WiredTriggerBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save }>
<div className="flex flex-col gap-1">
<Text bold>Action</Text>
<select className="form-select form-select-sm" value={ selectedAction } onChange={ event => setSelectedAction(parseInt(event.target.value)) }>
{ ACTION_OPTIONS.map(option => <option key={ option.value } value={ option.value }>{ LocalizeText(option.label) }</option>) }
</select>
</div>
{ (selectedAction === ACTION_SIGN) &&
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<input checked={ signFilterEnabled } className="form-check-input" id="signFilterEnabled" type="checkbox" onChange={ event => setSignFilterEnabled(event.target.checked) } />
<Text>{ LocalizeText('wiredfurni.params.sign_filter') }</Text>
</div>
{ signFilterEnabled &&
<select className="form-select form-select-sm" value={ signId } onChange={ event => setSignId(parseInt(event.target.value)) }>
{ SIGN_OPTIONS.map(option => <option key={ option.value } value={ option.value }>{ LocalizeText(option.label) }</option>) }
</select> }
</div> }
{ (selectedAction === ACTION_DANCE) &&
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<input checked={ danceFilterEnabled } className="form-check-input" id="danceFilterEnabled" type="checkbox" onChange={ event => setDanceFilterEnabled(event.target.checked) } />
<Text>{ LocalizeText('wiredfurni.params.dance_filter') }</Text>
</div>
{ danceFilterEnabled &&
<select className="form-select form-select-sm" value={ danceId } onChange={ event => setDanceId(parseInt(event.target.value)) }>
{ DANCE_OPTIONS.map(option => <option key={ option.value } value={ option.value }>{ LocalizeText(option.label) }</option>) }
</select> }
</div> }
</WiredTriggerBaseView>
);
};
+7 -7
View File
@@ -24,8 +24,8 @@ input[type=number] {
.btn-primary {
color: #fff;
background-color: #3c6d82;
border: 2px solid #1a617f;
background-color: var(--ui-btn-primary-bg, #3c6d82);
border: 2px solid var(--ui-btn-primary-border, #1a617f);
padding: 0.25rem 0.5rem;
font-size: .7875rem;
border-radius: 0.5rem;
@@ -33,7 +33,7 @@ input[type=number] {
}
.btn-primary:hover {
border: 2px solid #1a617f;
border: 2px solid var(--ui-btn-primary-border, #1a617f);
box-shadow: none!important;
}
@@ -81,16 +81,16 @@ input[type=number] {
.btn-dark {
color: #fff;
background-color: #212131;
border: 2px solid #1c1c2a;
background-color: var(--ui-dark-bg, #212131);
border: 2px solid var(--ui-dark-border, #1c1c2a);
box-shadow: none!important;
border-radius: 8px;
padding: 4px 11px 4px 11px;
}
.btn-dark:hover{
background-color: #212131;
border: 2px solid #1c1c2a;
background-color: var(--ui-dark-bg, #212131);
border: 2px solid var(--ui-dark-border, #1c1c2a);
box-shadow: none!important;
border-radius: 8px;
padding: 4px 11px 4px 11px;
+2 -2
View File
@@ -22,7 +22,7 @@
pointer-events: all;
}
.borderhccontent{
background-color: #212131;
background-color: var(--ui-dark-bg, #212131);
border-radius: 0.5rem!important;
border: 2px solid #383853;
height: calc(100% - 3px);
@@ -46,7 +46,7 @@
}
.nitro-purse-seasonal-currency {
background-color: #212131;
background-color: var(--ui-dark-bg, #212131);
background: linear-gradient(to right, #5f5f8d, transparent);
height: 30px;
margin-bottom: 4px;
+1 -1
View File
@@ -27,7 +27,7 @@
width: clamp(160px, 20vw, 190px); /* Responsive width */
z-index: 30;
pointer-events: auto;
background: #212131;
background: var(--ui-dark-bg, #212131);
box-shadow: inset 0 5px rgba(38, 38, 57, 0.6), inset 0 -4px rgba(25, 25, 37, 0.6);
border-radius: 0.5rem;
padding: 10px;
+3 -3
View File
@@ -4,7 +4,7 @@
left: 15px;
.nitro-room-tools {
background: #212131;
background: var(--ui-dark-bg, #212131);
box-shadow: inset 0px 5px lighten(rgba(#000, .6), 2.5), inset 0 -4px darken(rgba(#000, .6), 4);
border-top-right-radius: .25rem;
border-bottom-right-radius: .25rem;
@@ -54,7 +54,7 @@
}
.nitro-room-history {
background: #212131;
background: var(--ui-dark-bg, #212131);
box-shadow: inset 0px 5px lighten(rgba(#000, .6), 2.5), inset 0 -4px darken(rgba(#000, .6), 4);
transition: all .2s ease;
width: 150px;
@@ -63,7 +63,7 @@
}
.nitro-room-tools-info {
background: #212131;
background: var(--ui-dark-bg, #212131);
box-shadow: inset 0px 5px lighten(rgba(#000, .6), 2.5), inset 0 -4px darken(rgba(#000, .6), 4);
transition: all .2s ease;
max-width: 250px;
+23 -1
View File
@@ -1,4 +1,4 @@
import { AchievementNotificationMessageEvent, ActivityPointNotificationMessageEvent, ClubGiftNotificationEvent, ClubGiftSelectedEvent, ConnectionErrorEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, HabboBroadcastMessageEvent, HotelClosedAndOpensEvent, HotelClosesAndWillOpenAtEvent, HotelWillCloseInMinutesEvent, InfoFeedEnableMessageEvent, MaintenanceStatusMessageEvent, ModeratorCautionEvent, ModeratorMessageEvent, MOTDNotificationEvent, NotificationDialogMessageEvent, PetLevelNotificationEvent, PetReceivedMessageEvent, RespectReceivedEvent, RoomEnterEffect, RoomEnterEvent, SimpleAlertMessageEvent, UserBannedMessageEvent, Vector3d } from '@nitrots/nitro-renderer';
import { AchievementNotificationMessageEvent, ActivityPointNotificationMessageEvent, ClubGiftNotificationEvent, ClubGiftSelectedEvent, ConnectionErrorEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, HabboBroadcastMessageEvent, HotelClosedAndOpensEvent, HotelClosesAndWillOpenAtEvent, HotelWillCloseInMinutesEvent, InfoFeedEnableMessageEvent, MaintenanceStatusMessageEvent, ModeratorCautionEvent, ModeratorMessageEvent, MOTDNotificationEvent, NotificationDialogMessageEvent, PetLevelNotificationEvent, PetReceivedMessageEvent, RespectReceivedEvent, RoomEnterEffect, RoomEnterEvent, SimpleAlertMessageEvent, UserBannedMessageEvent, Vector3d, WiredRewardResultMessageEvent } from '@nitrots/nitro-renderer';
import { useCallback, useState } from 'react';
import { useBetween } from 'use-between';
import { GetConfigurationValue, LocalizeBadgeName, LocalizeText, NotificationAlertItem, NotificationAlertType, NotificationBubbleItem, NotificationBubbleType, NotificationConfirmItem, PlaySound, ProductImageUtility, TradingNotificationType } from '../../api';
@@ -397,6 +397,28 @@ const useNotificationState = () =>
simpleAlert(LocalizeText(parser.alertMessage), NotificationAlertType.DEFAULT, null, null, LocalizeText(parser.titleMessage ? parser.titleMessage : 'notifications.broadcast.title'));
});
useMessageEvent<WiredRewardResultMessageEvent>(WiredRewardResultMessageEvent, event =>
{
const parser = event.getParser();
switch(parser.reason)
{
case WiredRewardResultMessageEvent.PRODUCT_DONATED_CODE:
case WiredRewardResultMessageEvent.BADGE_DONATED_CODE:
simpleAlert(LocalizeText('wiredfurni.rewardsuccess.body'), NotificationAlertType.DEFAULT, null, null, LocalizeText('wiredfurni.rewardsuccess.title'));
return;
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 8:
simpleAlert(LocalizeText(`wiredfurni.rewardfailed.reason.${ parser.reason }`), NotificationAlertType.DEFAULT, null, null, LocalizeText('wiredfurni.rewardfailed.title'));
return;
}
});
const onRoomEnterEvent = useCallback(() =>
{
if(modDisclaimerShown) return;
+4 -3
View File
@@ -123,9 +123,10 @@ const useChatWidgetState = () =>
text = LocalizeText('widget.chatbubble.handitem', ['username', 'handitem'], [username, LocalizeText(('handitem' + event.extraParam))]);
break;
case RoomSessionChatEvent.CHAT_TYPE_MUTE_REMAINING: {
const hours = ((event.extraParam > 0) ? Math.floor((event.extraParam / 3600)) : 0).toString();
const minutes = ((event.extraParam > 0) ? Math.floor((event.extraParam % 3600) / 60) : 0).toString();
const seconds = (event.extraParam % 60).toString();
const remainingSeconds = Math.max(0, event.extraParam);
const hours = Math.floor(remainingSeconds / 3600).toString();
const minutes = Math.floor((remainingSeconds % 3600) / 60).toString();
const seconds = (remainingSeconds % 60).toString();
text = LocalizeText('widget.chatbubble.mutetime', ['hours', 'minutes', 'seconds'], [hours, minutes, seconds]);
break;