Merge pull request #133 from duckietm/Dev

🆙 Update About screen (needs a emu change as well)
This commit is contained in:
DuckieTM
2026-05-17 09:59:08 +02:00
committed by GitHub
6 changed files with 486 additions and 1 deletions
@@ -1,6 +1,9 @@
{
"socket.url": "wss://nitro.example.com:2096",
"api.url": "https://nitro.example.com:2096",
"crypto.ws.enabled": false,
"crypto.ws.signing.enabled": false,
"crypto.ws.signing.public_key": "",
"asset.url": "https://hotel.example.com/client/nitro/bundled",
"image.library.url": "https://hotel.example.com/client/c_images/",
"hof.furni.url": "https://hotel.example.com/client/c_images/dcr/hof_furni",
@@ -5,6 +5,7 @@ export class NotificationAlertType
public static MODERATION: string = 'moderation';
public static EVENT: string = 'event';
public static NITRO: string = 'nitro';
public static NITRO_INFO: string = 'nitro-info';
public static SEARCH: string = 'search';
public static ALERT: string = 'alert';
}
@@ -1,4 +1,5 @@
import { NotificationAlertItem, NotificationAlertType } from '../../../../api';
import { NitroInfoAlertView } from './NitroInfoAlertView';
import { NitroSystemAlertView } from './NitroSystemAlertView';
import { NotificationDefaultAlertView } from './NotificationDefaultAlertView';
import { NotificationSeachAlertView } from './NotificationSearchAlertView';
@@ -14,6 +15,8 @@ export const GetAlertLayout = (item: NotificationAlertItem, onClose: () => void)
{
case NotificationAlertType.NITRO:
return <NitroSystemAlertView key={key} {...props} />;
case NotificationAlertType.NITRO_INFO:
return <NitroInfoAlertView key={key} {...props} />;
case NotificationAlertType.SEARCH:
return <NotificationSeachAlertView key={key} {...props} />;
default:
@@ -0,0 +1,154 @@
import { FC, useMemo } from 'react';
import { LocalizeText, NotificationAlertItem, NotificationAlertType, OpenUrl } from '../../../../api';
import { Button, Column, Flex, LayoutAvatarImageView, LayoutNotificationAlertView, LayoutNotificationAlertViewProps, Text } from '../../../../common';
const INFO_AVATAR_FIGURE = 'hr-831-61.hd-180-2.lg-270-100.sh-290-110.ha-3129-100.fa-1205-63.cc-3039-100';
interface NitroInfoAlertViewProps extends LayoutNotificationAlertViewProps
{
item: NotificationAlertItem;
}
const REPORT_ISSUES_URL = 'https://github.com/duckietm/Nitro-V3/issues';
type InfoSection = { title: string; lines: string[]; kind: 'hotel' | 'server' | 'credits' | 'generic' };
const detectKind = (title: string): InfoSection['kind'] =>
{
const t = title.toLowerCase();
if(t.includes('hotel')) return 'hotel';
if(t.includes('server')) return 'server';
if(t.includes('credit')) return 'credits';
return 'generic';
};
const parseInfoSections = (text: string): { version: string; sections: InfoSection[] } =>
{
const version = (text.match(/<b>([^<]+)<\/b>/) || [ '', '' ])[1];
const stripped = text
.replace(/^<b>[^<]+<\/b>\r?\n?/, '')
.replace(/Report issues at:[^]*$/, '');
const sections: InfoSection[] = [];
const blocks = stripped.split(/\r?\n+/);
for(const block of blocks)
{
if(!block.trim()) continue;
const headerMatch = block.match(/<b>([^<]+)<\/b>/);
if(!headerMatch) continue;
const title = headerMatch[1];
const rest = block.substring(block.indexOf('</b>') + 4);
const lines = rest.split(/\r/).map(l => l.trim().replace(/^-\s*/, '')).filter(Boolean);
sections.push({ title, lines, kind: detectKind(title) });
}
return { version, sections };
};
const sectionIcon: Record<InfoSection['kind'], string> = {
hotel: '🏨',
server: '🖥️',
credits: '⭐',
generic: '️'
};
const splitLabel = (line: string): { label: string; value: string } =>
{
const idx = line.indexOf(':');
if(idx === -1) return { label: '', value: line };
return { label: line.substring(0, idx).trim(), value: line.substring(idx + 1).trim() };
};
export const NitroInfoAlertView: FC<NitroInfoAlertViewProps> = props =>
{
const { item = null, title: titleProp = null, onClose = null, ...rest } = props;
const { version, sections } = useMemo(() =>
{
const text = (item && item.messages && item.messages[0]) || '';
return parseInfoSections(text);
}, [ item ]);
const rawTitle = titleProp || (item && item.title) || '';
const displayTitle = (rawTitle && rawTitle !== 'nitro.info.title') ? rawTitle : 'Hotel Info';
return (
<LayoutNotificationAlertView
title={ displayTitle }
onClose={ onClose }
{ ...rest }
type={ NotificationAlertType.NITRO_INFO }>
<div className="nitro-info-hero">
<div className="nitro-info-hero-stars" />
{ version &&
<div className="nitro-info-version-badge">
<span className="nitro-info-version-spark"></span>
<span className="nitro-info-version-text">{ version }</span>
<span className="nitro-info-version-spark"></span>
</div> }
</div>
<Flex fullHeight gap={ 2 } overflow="hidden" className="nitro-info-content">
<div className="nitro-info-avatar-wrap shrink-0">
<LayoutAvatarImageView
figure={ INFO_AVATAR_FIGURE }
direction={ 2 }
classNames={ [ 'nitro-info-avatar' ] } />
<div className="nitro-info-avatar-shadow" />
</div>
<Column fullWidth gap={ 2 } overflow="auto" className="nitro-info-body">
{ sections.map((section, index) => (
<div key={ index } className={ `nitro-info-section nitro-info-section-${ section.kind }` }>
<div className="nitro-info-section-header">
<span className="nitro-info-section-icon">{ sectionIcon[section.kind] }</span>
<span className="nitro-info-section-title">{ section.title }</span>
</div>
<div className="nitro-info-section-body">
{ section.kind === 'credits'
? (
<ul className="nitro-info-credits-list">
{ section.lines.map((line, i) => (
<li key={ i }>
<span className="nitro-info-credit-star"></span>
<span>{ line }</span>
</li>
)) }
</ul>
)
: (
<ul className="nitro-info-stats-list">
{ section.lines.map((line, i) =>
{
const { label, value } = splitLabel(line);
return (
<li key={ i }>
{ label && <span className="nitro-info-stat-label">{ label }</span> }
<span className="nitro-info-stat-value">{ value }</span>
</li>
);
}) }
</ul>
) }
</div>
</div>
)) }
<div className="nitro-info-footer">
<Text center small italics>Found a bug? Help us improve!</Text>
<Flex gap={ 1 } fullWidth className="nitro-info-actions">
<Button fullWidth variant="success" className="nitro-info-report-btn" onClick={ () => OpenUrl(REPORT_ISSUES_URL) }>
<span>🐞 Report Issues</span>
</Button>
<Button fullWidth onClick={ onClose }>
{ LocalizeText('generic.close') }
</Button>
</Flex>
</div>
</Column>
</Flex>
</LayoutNotificationAlertView>
);
};
@@ -47,6 +47,320 @@
min-width: auto;
}
}
&.nitro-alert-nitro-info {
width: 460px;
min-height: 320px;
max-height: 640px;
animation: nitroInfoPop 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
.nitro-info-hero {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
margin: -8px -8px 6px -8px;
padding: 14px 10px 18px 10px;
background:
radial-gradient(ellipse at top, rgba(255, 220, 120, 0.45) 0%, transparent 60%),
linear-gradient(135deg, #4a72b8 0%, #2d4a82 45%, #5a3d9a 100%);
border-bottom: 2px solid #1c2a4a;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.25),
inset 0 -3px 0 rgba(0, 0, 0, 0.25);
overflow: hidden;
}
.nitro-info-hero-stars {
position: absolute;
inset: 0;
background-image:
radial-gradient(2px 2px at 18% 28%, rgba(255, 255, 255, 0.85), transparent 60%),
radial-gradient(1.5px 1.5px at 72% 18%, rgba(255, 255, 255, 0.7), transparent 60%),
radial-gradient(1px 1px at 42% 65%, rgba(255, 255, 255, 0.9), transparent 60%),
radial-gradient(1.5px 1.5px at 88% 78%, rgba(255, 255, 255, 0.75), transparent 60%),
radial-gradient(1px 1px at 12% 80%, rgba(255, 255, 255, 0.8), transparent 60%);
opacity: 0.85;
animation: nitroInfoTwinkle 2.6s ease-in-out infinite alternate;
pointer-events: none;
}
.nitro-info-version-badge {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
margin: 0 auto;
padding: 6px 18px;
background: linear-gradient(180deg, #ffeb8a 0%, #ffd54d 50%, #f0a318 100%);
border: 2px solid #8a5b00;
border-radius: 18px;
color: #4a2b00;
font-weight: 700;
font-size: 14px;
letter-spacing: 0.4px;
text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.7),
inset 0 -2px 0 rgba(140, 75, 0, 0.4),
0 3px 0 rgba(0, 0, 0, 0.25),
0 0 18px rgba(255, 200, 80, 0.55);
width: max-content;
max-width: 90%;
}
.nitro-info-version-spark {
color: #fff;
text-shadow: 0 0 6px rgba(255, 255, 200, 0.9);
animation: nitroInfoSpin 3s linear infinite;
display: inline-block;
}
.nitro-info-version-spark:last-child {
animation-direction: reverse;
}
.nitro-info-content {
padding: 0 2px;
}
.nitro-info-avatar-wrap {
position: relative;
width: 90px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding-top: 2px;
}
.nitro-info-avatar {
filter: drop-shadow(0 3px 5px rgba(0, 0, 0, 0.35));
animation: nitroInfoBob 2.6s ease-in-out infinite;
}
.nitro-info-avatar-shadow {
width: 60px;
height: 8px;
margin-top: -4px;
background: radial-gradient(ellipse, rgba(0, 0, 0, 0.4) 0%, transparent 70%);
animation: nitroInfoShadowPulse 2.6s ease-in-out infinite;
}
.nitro-info-body {
font-family: Volter, Volter_Goldfish, "Ubuntu", sans-serif;
color: #2f2f2f;
padding-right: 4px;
}
.nitro-info-section {
background: linear-gradient(to bottom, #ffffff 0%, #eaf1fb 100%);
border: 1px solid #6f8db5;
border-radius: 6px;
overflow: hidden;
flex-shrink: 0;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.8),
0 2px 0 rgba(0, 0, 0, 0.12);
transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.nitro-info-section:hover {
transform: translateY(-1px);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.8),
0 3px 0 rgba(0, 0, 0, 0.18),
0 0 0 1px rgba(110, 160, 230, 0.4);
}
.nitro-info-section-header {
display: flex;
align-items: center;
gap: 6px;
color: #ffffff;
font-weight: bold;
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.45);
padding: 4px 10px;
letter-spacing: 0.3px;
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
}
.nitro-info-section-icon {
font-size: 14px;
filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.4));
}
.nitro-info-section-hotel .nitro-info-section-header {
background: linear-gradient(180deg, #4fb3ff 0%, #1f6dc7 100%);
}
.nitro-info-section-server .nitro-info-section-header {
background: linear-gradient(180deg, #6bd66b 0%, #2a8a2a 100%);
}
.nitro-info-section-credits .nitro-info-section-header {
background: linear-gradient(180deg, #ff9a44 0%, #d9591a 100%);
}
.nitro-info-section-generic .nitro-info-section-header {
background: linear-gradient(180deg, #8da0bc 0%, #4b5d7a 100%);
}
.nitro-info-section-body {
padding: 6px 10px;
color: #1f2f4a;
}
.nitro-info-stats-list,
.nitro-info-credits-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.nitro-info-stats-list li {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
padding: 2px 4px;
border-radius: 3px;
}
.nitro-info-stats-list li:nth-child(odd) {
background: rgba(110, 160, 230, 0.08);
}
.nitro-info-stat-label {
font-size: 12px;
color: #4a5a76;
font-weight: 500;
flex-shrink: 0;
}
.nitro-info-stat-value {
font-size: 12px;
color: #1a3a6b;
font-weight: 700;
background: linear-gradient(180deg, #ffffff 0%, #e6efff 100%);
border: 1px solid #b8cce6;
border-radius: 4px;
padding: 1px 8px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
text-align: right;
word-break: break-word;
}
.nitro-info-credits-list li {
display: flex;
align-items: flex-start;
gap: 6px;
padding: 3px 4px;
font-size: 12px;
color: #4a2b00;
border-radius: 3px;
transition: background 0.15s ease;
word-break: break-word;
}
.nitro-info-credits-list li:hover {
background: rgba(255, 180, 80, 0.15);
}
.nitro-info-credit-star {
color: #f0a318;
text-shadow: 0 0 4px rgba(240, 163, 24, 0.6);
font-size: 13px;
flex-shrink: 0;
}
.nitro-info-footer {
margin-top: 6px;
padding-top: 8px;
border-top: 1px dashed #b0b0b0;
display: flex;
flex-direction: column;
gap: 6px;
}
.nitro-info-actions {
margin-top: 2px;
}
.nitro-info-report-btn {
position: relative;
overflow: hidden;
background: linear-gradient(180deg, #ff6b6b 0%, #c92a2a 100%) !important;
border-color: #8a1a1a !important;
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.35);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.3),
inset 0 -2px 0 rgba(0, 0, 0, 0.25),
0 0 12px rgba(255, 100, 100, 0.4);
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.nitro-info-report-btn:hover {
transform: translateY(-1px);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.4),
inset 0 -2px 0 rgba(0, 0, 0, 0.25),
0 2px 0 rgba(0, 0, 0, 0.2),
0 0 18px rgba(255, 100, 100, 0.7);
}
.nitro-info-report-btn:active {
transform: translateY(1px);
}
.nitro-info-report-btn::after {
content: '';
position: absolute;
top: 0;
left: -120%;
width: 60%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
transform: skewX(-20deg);
animation: nitroInfoShine 2.8s ease-in-out infinite;
}
}
}
@keyframes nitroInfoPop {
0% { transform: scale(0.85); opacity: 0; }
60% { transform: scale(1.04); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes nitroInfoTwinkle {
0% { opacity: 0.4; }
100% { opacity: 1; }
}
@keyframes nitroInfoSpin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes nitroInfoBob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-3px); }
}
@keyframes nitroInfoShadowPulse {
0%, 100% { transform: scaleX(1); opacity: 0.55; }
50% { transform: scaleX(0.8); opacity: 0.35; }
}
@keyframes nitroInfoShine {
0% { left: -120%; }
60%, 100% { left: 140%; }
}
.nitro-notification-bubble {
+11 -1
View File
@@ -212,8 +212,18 @@ const useNotificationState = () =>
useMessageEvent<HabboBroadcastMessageEvent>(HabboBroadcastMessageEvent, event =>
{
const parser = event.getParser();
const raw = parser.message.replace(/\\r/g, '\r');
simpleAlert(parser.message.replace(/\\r/g, '\r'), null, null, LocalizeText('notifications.broadcast.title'));
const sentinel = '[NITRO_INFO_V1]';
if(raw.startsWith(sentinel))
{
const body = raw.substring(sentinel.length).replace(/^[\r\n]+/, '');
simpleAlert(body, NotificationAlertType.NITRO_INFO, null, null, LocalizeText('nitro.info.title'));
return;
}
simpleAlert(raw, null, null, LocalizeText('notifications.broadcast.title'));
});
useMessageEvent<AchievementNotificationMessageEvent>(AchievementNotificationMessageEvent, event =>