mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
Merge branch 'main' into pr/wired-freeze-furni-movement-ui-clean-20260318
This commit is contained in:
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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'
|
||||
];
|
||||
@@ -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);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './IUiSettings';
|
||||
export * from './UiSettingsContext';
|
||||
@@ -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
@@ -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
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 ...
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user