mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
Merge pull request #133 from duckietm/Dev
🆙 Update About screen (needs a emu change as well)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
Reference in New Issue
Block a user