From e209146f47937fccaaa400994676ea7d352ec2d0 Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Sun, 17 May 2026 09:58:38 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=86=99=20Update=20About=20screen=20(needs?= =?UTF-8?q?=20a=20emu=20change=20as=20well)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/configuration/renderer-config.example | 3 + src/api/notification/NotificationAlertType.ts | 1 + .../views/alert-layouts/GetAlertLayout.tsx | 3 + .../alert-layouts/NitroInfoAlertView.tsx | 154 +++++++++ .../notification/NotificationCenterView.css | 314 ++++++++++++++++++ src/hooks/notification/useNotification.ts | 12 +- 6 files changed, 486 insertions(+), 1 deletion(-) create mode 100644 src/components/notification-center/views/alert-layouts/NitroInfoAlertView.tsx diff --git a/public/configuration/renderer-config.example b/public/configuration/renderer-config.example index f8d94bf..6c928da 100644 --- a/public/configuration/renderer-config.example +++ b/public/configuration/renderer-config.example @@ -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", diff --git a/src/api/notification/NotificationAlertType.ts b/src/api/notification/NotificationAlertType.ts index ad804e8..7c4c633 100644 --- a/src/api/notification/NotificationAlertType.ts +++ b/src/api/notification/NotificationAlertType.ts @@ -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'; } diff --git a/src/components/notification-center/views/alert-layouts/GetAlertLayout.tsx b/src/components/notification-center/views/alert-layouts/GetAlertLayout.tsx index e19da0f..b41d757 100644 --- a/src/components/notification-center/views/alert-layouts/GetAlertLayout.tsx +++ b/src/components/notification-center/views/alert-layouts/GetAlertLayout.tsx @@ -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 ; + case NotificationAlertType.NITRO_INFO: + return ; case NotificationAlertType.SEARCH: return ; default: diff --git a/src/components/notification-center/views/alert-layouts/NitroInfoAlertView.tsx b/src/components/notification-center/views/alert-layouts/NitroInfoAlertView.tsx new file mode 100644 index 0000000..0bbabde --- /dev/null +++ b/src/components/notification-center/views/alert-layouts/NitroInfoAlertView.tsx @@ -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>/) || [ '', '' ])[1]; + + const stripped = text + .replace(/^[^<]+<\/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>/); + if(!headerMatch) continue; + + const title = headerMatch[1]; + const rest = block.substring(block.indexOf('') + 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 = { + 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 = 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 ( + +
+
+ { version && +
+ âœĻ + { version } + âœĻ +
} +
+ +
+ +
+
+ + { sections.map((section, index) => ( +
+
+ { sectionIcon[section.kind] } + { section.title } +
+
+ { section.kind === 'credits' + ? ( +
    + { section.lines.map((line, i) => ( +
  • + ★ + { line } +
  • + )) } +
+ ) + : ( +
    + { section.lines.map((line, i) => + { + const { label, value } = splitLabel(line); + return ( +
  • + { label && { label } } + { value } +
  • + ); + }) } +
+ ) } +
+
+ )) } +
+ Found a bug? Help us improve! + + + + +
+
+ + + ); +}; diff --git a/src/css/notification/NotificationCenterView.css b/src/css/notification/NotificationCenterView.css index ec2c5e5..c24cb67 100644 --- a/src/css/notification/NotificationCenterView.css +++ b/src/css/notification/NotificationCenterView.css @@ -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 { diff --git a/src/hooks/notification/useNotification.ts b/src/hooks/notification/useNotification.ts index 66ac864..9ef60cf 100644 --- a/src/hooks/notification/useNotification.ts +++ b/src/hooks/notification/useNotification.ts @@ -212,8 +212,18 @@ const useNotificationState = () => useMessageEvent(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, event =>