🆙 Take #3 desktop view catalog is now 100%

This commit is contained in:
duckietm
2026-06-05 16:31:59 +02:00
parent f4d41dd3c9
commit fff4c0bca6
8 changed files with 283 additions and 213 deletions
+29 -1
View File
@@ -1,4 +1,4 @@
import { GetRenderer, GetTicker, NitroTicker, RoomPreviewer, TextureUtils } from '@nitrots/nitro-renderer';
import { GetRenderer, GetTicker, NitroLogger, NitroTicker, RoomPreviewer, TextureUtils } from '@nitrots/nitro-renderer';
import { FC, MouseEvent, useEffect, useRef } from 'react';
export const LayoutRoomPreviewerView: FC<{
@@ -8,6 +8,13 @@ export const LayoutRoomPreviewerView: FC<{
{
const { roomPreviewer = null, height = 0 } = props;
const elementRef = useRef<HTMLDivElement>(null);
// Latch that disables further renders once Pixi throws inside this
// previewer. The crash (e.g. blackhole furni's filter chain that
// accesses .alphaMode on a null texture) repeats every animation
// frame as long as the ticker keeps firing, flooding the console
// and locking the catalog. One catch and we stop trying for the
// lifetime of this previewer instance.
const renderFailedRef = useRef(false);
const onClick = (event: MouseEvent<HTMLDivElement>) =>
{
@@ -21,17 +28,22 @@ export const LayoutRoomPreviewerView: FC<{
{
if(!elementRef) return;
renderFailedRef.current = false;
const width = elementRef.current.parentElement.clientWidth;
const texture = TextureUtils.createRenderTexture(width, height);
const paintToDOM = () =>
{
if(renderFailedRef.current) return;
if(!roomPreviewer || !elementRef.current) return;
const renderingCanvas = roomPreviewer.getRenderingCanvas();
if(!renderingCanvas) return;
try
{
GetRenderer().render({
target: texture,
container: renderingCanvas.master,
@@ -45,13 +57,29 @@ export const LayoutRoomPreviewerView: FC<{
canvas.height = 0;
elementRef.current.style.backgroundImage = `url(${ base64 })`;
}
catch(error)
{
renderFailedRef.current = true;
NitroLogger.error('LayoutRoomPreviewerView paint failed; disabling further renders for this preview', error);
}
};
const update = (ticker: NitroTicker) =>
{
if(renderFailedRef.current) return;
if(!roomPreviewer || !elementRef.current) return;
try
{
roomPreviewer.updatePreviewRoomView();
}
catch(error)
{
renderFailedRef.current = true;
NitroLogger.error('LayoutRoomPreviewerView update failed; disabling further renders for this preview', error);
return;
}
const renderingCanvas = roomPreviewer.getRenderingCanvas();
@@ -1,8 +1,8 @@
import { ColorConverter } from '@nitrots/nitro-renderer';
import { FC, useMemo, useState } from 'react';
import { FaFillDrip } from 'react-icons/fa';
import { IPurchasableOffer, SanitizeHtml } from '../../../../../api';
import { AutoGrid, Button, Column, Grid, LayoutGridItem, Text } from '../../../../../common';
import { FaExchangeAlt, FaFillDrip, FaSyncAlt } from 'react-icons/fa';
import { IPurchasableOffer, ProductTypeEnum, SanitizeHtml } from '../../../../../api';
import { AutoGrid, Button, Column, LayoutGridItem, Text } from '../../../../../common';
import { useCatalogData, useCatalogUiState } from '../../../../../hooks';
import { CatalogGridOfferView } from '../common/CatalogGridOfferView';
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
@@ -22,7 +22,7 @@ export const CatalogLayoutColorGroupingView: FC<CatalogLayoutColorGroupViewProps
{
const { page = null } = props;
const [ colorableItems, setColorableItems ] = useState<Map<string, number[]>>(new Map<string, number[]>());
const { currentOffer = null } = useCatalogData();
const { currentOffer = null, roomPreviewer = null } = useCatalogData();
const { setCurrentOffer = null } = useCatalogUiState();
const [ colorsShowing, setColorsShowing ] = useState<boolean>(false);
@@ -132,26 +132,42 @@ export const CatalogLayoutColorGroupingView: FC<CatalogLayoutColorGroupViewProps
}, [ page.offers ]);
return (
<Grid>
<Column overflow="hidden" size={ 7 }>
<AutoGrid columnCount={ 5 }>
<Column overflow="hidden">
{ /* Top: two visible rows of furni tiles. Tile is 70px tall
and the AutoGrid handles its own overflow if there are
more than two rows worth of offers. */ }
<div className="shrink-0" style={ { maxHeight: 154 } }>
{ (!colorsShowing || !currentOffer || !colorableItems.has(currentOffer.product.furnitureData.className)) &&
offers.map((offer, index) => <CatalogGridOfferView key={ index } itemActive={ (currentOffer && (currentOffer.product.furnitureData.hasIndexedColor ? currentOffer.product.furnitureData.className === offer.product.furnitureData.className : currentOffer.offerId === offer.offerId)) } offer={ offer } selectOffer={ selectOffer } />)
}
<AutoGrid columnCount={ 7 } columnMinHeight={ 70 } columnMinWidth={ 45 }>
{ offers.map((offer, index) => <CatalogGridOfferView key={ index } itemActive={ (currentOffer && (currentOffer.product.furnitureData.hasIndexedColor ? currentOffer.product.furnitureData.className === offer.product.furnitureData.className : currentOffer.offerId === offer.offerId)) } offer={ offer } selectOffer={ selectOffer } />) }
</AutoGrid> }
{ (colorsShowing && currentOffer && colorableItems.has(currentOffer.product.furnitureData.className)) &&
colorableItems.get(currentOffer.product.furnitureData.className).map((color, index) => <LayoutGridItem key={ index } itemHighlight className="clear-bg" itemActive={ (currentOffer.product.furnitureData.colorIndex === index) } itemColor={ ColorConverter.int2rgb(color) } onClick={ event => selectColor(index, currentOffer.product.furnitureData.className) } />)
}
</AutoGrid>
</Column>
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
<div className="nitro-catalog-classic-color-swatches flex flex-wrap gap-1 p-2 overflow-auto">
{ colorableItems.get(currentOffer.product.furnitureData.className).map((color, index) => <LayoutGridItem key={ index } itemHighlight className="clear-bg" itemActive={ (currentOffer.product.furnitureData.colorIndex === index) } itemColor={ ColorConverter.int2rgb(color) } onClick={ event => selectColor(index, currentOffer.product.furnitureData.className) } />) }
</div> }
</div>
{ /* Bottom: preview pane stacked under the grid. Mirrors the
default-3x3 split (preview on the left, offer info on the
right) so the rotate/state buttons and Buy/Gift actions
sit where the user expects. */ }
{ !currentOffer &&
<>
<Column center grow overflow="hidden">
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: SanitizeHtml(page.localization.getText(0)) } } />
</> }
</Column> }
{ currentOffer &&
<div className="nitro-catalog-classic-offer-panel flex flex-col items-center grow overflow-hidden gap-2">
<div className="nitro-catalog-classic-offer-preview relative flex items-center justify-center overflow-hidden">
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) &&
<>
<div className="relative overflow-hidden">
<button className="nitro-catalog-classic-preview-btn nitro-catalog-classic-preview-rotate" onClick={ () => roomPreviewer?.changeRoomObjectDirection() }>
<FaSyncAlt />
</button>
<button className="nitro-catalog-classic-preview-btn nitro-catalog-classic-preview-state" onClick={ () => roomPreviewer?.changeRoomObjectState() }>
<FaExchangeAlt />
</button>
</> }
<CatalogViewProductWidgetView />
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 inset-e-1" position="absolute" />
{ currentOffer.product.furnitureData.hasIndexedColor &&
@@ -159,19 +175,16 @@ export const CatalogLayoutColorGroupingView: FC<CatalogLayoutColorGroupViewProps
<FaFillDrip className="fa-icon" />
</Button> }
</div>
<Column className="grow!" gap={ 1 }>
<div className="w-full max-w-[360px] flex flex-col gap-2 px-1">
<CatalogLimitedItemWidgetView />
<Text truncate className="grow!">{ currentOffer.localizationName }</Text>
<div className="flex justify-between">
<div className="flex flex-col gap-1">
<Text truncate>{ currentOffer.localizationName }</Text>
<div className="flex justify-between items-center">
<CatalogSpinnerWidgetView />
</div>
<CatalogTotalPriceWidget alignItems="end" justifyContent="end" />
</div>
<CatalogPurchaseWidgetView />
</div>
</div> }
</Column>
</> }
</Column>
</Grid>
);
};
@@ -291,7 +291,7 @@ export const CatalogLayoutPetView: FC<CatalogLayoutProps> = props =>
{ LocalizeText('catalog.pets.back.breeds') }
</button> }
</div>
<div className="grid grid-cols-6 gap-1">
<div className={ colorsShowing ? 'nitro-catalog-classic-color-swatches flex flex-wrap gap-1 p-2 overflow-auto' : 'grid grid-cols-6 gap-1' }>
{ !colorsShowing && (sellablePalettes.length > 0) && sellablePalettes.map((palette, index) => (
<LayoutGridItem
key={ index }
@@ -303,10 +303,12 @@ export const CatalogLayoutPetView: FC<CatalogLayoutProps> = props =>
</LayoutGridItem>
)) }
{ colorsShowing && (sellableColors.length > 0) && sellableColors.map((colorSet, index) => (
<div
<LayoutGridItem
key={ index }
className={ `w-full aspect-square rounded border-2 cursor-pointer transition-all ${ selectedColorIndex === index ? 'border-primary scale-110 shadow-md' : 'border-card-grid-item-border hover:border-primary/50' }` }
style={ { backgroundColor: `#${ ColorConverter.int2rgb(colorSet[0]) }` } }
itemHighlight
className="clear-bg"
itemActive={ (selectedColorIndex === index) }
itemColor={ ColorConverter.int2rgb(colorSet[0]) }
onClick={ () => setSelectedColorIndex(index) }
/>
)) }
@@ -22,6 +22,8 @@ export const CatalogViewProductWidgetView: FC<{}> = props =>
roomPreviewer.updateObjectRoom('default', 'default', 'default');
roomPreviewer.updateRoomWallsAndFloorVisibility(true, true);
const populate = () =>
{
switch(product.productType)
{
case ProductTypeEnum.FLOOR: {
@@ -101,6 +103,17 @@ export const CatalogViewProductWidgetView: FC<{}> = props =>
roomPreviewer.addAvatarIntoRoom(GetSessionDataManager().figure, product.productClassId);
return;
}
};
populate();
// RoomPreviewer.addFurnitureIntoRoom / addAvatarIntoRoom flip
// _automaticStateChange to true, which makes the ticker advance
// the room object's state every AUTOMATIC_STATE_CHANGE_INTERVAL.
// In the catalog we want the preview to sit still until the
// user clicks the state button explicitly - turn it back off
// after populate() runs.
roomPreviewer.setAutomaticStateChange(false);
}, [ currentOffer, previewStuffData, roomPreviewer ]);
if(!currentOffer) return null;
@@ -119,5 +132,11 @@ export const CatalogViewProductWidgetView: FC<{}> = props =>
);
}
return <LayoutRoomPreviewerView height={ 240 } roomPreviewer={ roomPreviewer } />;
// Re-mount the previewer whenever the offer changes so the render
// latch / texture handle in LayoutRoomPreviewerView resets cleanly.
// Without this a single broken offer (e.g. blackhole's Pixi filter
// crash) latches the previewer permanently and every following
// offer paints nothing - the singleton roomPreviewer + 240px height
// keep the same component mounted otherwise.
return <LayoutRoomPreviewerView key={ currentOffer?.offerId } height={ 240 } roomPreviewer={ roomPreviewer } />;
};
+94 -15
View File
@@ -12,14 +12,6 @@
--catalog-swf-select-outer: #82d1ed;
--catalog-swf-bc: #ff8d00;
--catalog-swf-bc-outer: #ffb53c;
--habbo-skin-ubuntu: url("../../assets/images/catalog/swf/skins/habbo_skin_ubuntu.png");
--habbo-skin-blue: url("../../assets/images/catalog/swf/skins/habbo_skin_blue.png");
--habbo-skin-illumina-light: url("../../assets/images/catalog/swf/skins/habbo_skin_illumina_light.png");
--habbo-skin-illumina-dark: url("../../assets/images/catalog/swf/skins/habbo_skin_illumina_dark.png");
--habbo-slice-frame: url("../../assets/images/catalog/swf/ubuntu_frame3_26x55.png");
--habbo-slice-tab-default: url("../../assets/images/catalog/swf/ubuntu_tab3_default_22x32.png");
--habbo-slice-tab-selected: url("../../assets/images/catalog/swf/ubuntu_tab3_selected_22x32.png");
--habbo-slice-tab-hover: url("../../assets/images/catalog/swf/ubuntu_tab3_hover_22x32.png");
/* Light gray secondary button - cropped from catalog_skin1.png
at (10, 190, 25x22). Drives the gift button "Cadeau", the
preview-room control button and the generic .nitro-catalog-swf-
@@ -42,10 +34,6 @@
--habbo-button-green-hover: url("../../assets/images/catalog/buttons/buy_hover.png");
--habbo-button-green-pressed: url("../../assets/images/catalog/buttons/buy_pressed.png");
--habbo-button-green-disabled: url("../../assets/images/catalog/buttons/buy_disabled.png");
--habbo-grid-default: url("../../assets/images/catalog/swf/habbo_grid.png");
--habbo-grid-hover: url("../../assets/images/catalog/swf/habbo_grid_hover.png");
--habbo-grid-selected: url("../../assets/images/catalog/swf/habbo_grid_selected.png");
--habbo-grid-selected-inactive: url("../../assets/images/catalog/swf/habbo_grid_selected_inactive.png");
--habbo-close: url("../../assets/images/catalog/buttons/close.png");
--habbo-close-hover: url("../../assets/images/catalog/buttons/close_hover.png");
--habbo-close-pressed: url("../../assets/images/catalog/buttons/close_pressed.png");
@@ -782,14 +770,24 @@
.nitro-catalog-classic-offer-preview {
position: relative;
width: 360px;
min-width: 360px;
height: 100%;
padding: 0;
overflow: hidden;
background: #000;
}
/* The default-3x3 layout puts the preview next to .offer-info inside
.offer-panel and needs the 360px column. Scope the pin to that
context so other layouts (color-grouping, etc.) can put the same
preview class inside a flex/grid column and let it track the
container width. Without this scoping the absolute-positioned
rotate/state buttons sit past the column's right edge and get
clipped by overflow: hidden. */
.nitro-catalog-classic-offer-panel > .nitro-catalog-classic-offer-preview {
width: 360px;
min-width: 360px;
}
.nitro-catalog-classic-preview-title {
position: absolute;
top: 12px;
@@ -892,12 +890,19 @@
min-height: var(--nitro-grid-column-min-height, 70px) !important;
border: 0 !important;
border-radius: 0 !important;
background-color: transparent !important;
background-image: none !important;
box-shadow: none !important;
overflow: visible !important;
}
/* Furni tiles drive their look from the icon image and need a clear
background. Color-grouping swatches use itemHighlight (.has-highlight)
to ask LayoutGridItem for a solid colour via inline backgroundColor -
keep the transparent override off those so the swatch is visible. */
.nitro-catalog-classic-window .layout-grid-item:not(.has-highlight) {
background-color: transparent !important;
}
.nitro-catalog-classic-window .layout-grid-item:hover {
background-image: none !important;
box-shadow: inset 0 0 0 1px #a1a19b !important;
@@ -911,6 +916,42 @@
inset -2px -2px 0 #ecece4 !important;
}
/* Habbo-classic colour swatches: small chip with a 1px dark border
and a subtle inner highlight so light tones still read as buttons.
Hover lifts the border; the selected swatch is "pressed" with a
sunken inner shadow and a bright cyan ring matching the catalog
selection accent. The cream inset from the generic .is-active rule
above would wash out the swatch colour, so we replace it here. */
.nitro-catalog-classic-window .layout-grid-item.has-highlight {
width: 26px !important;
height: 26px !important;
min-width: 26px !important;
min-height: 26px !important;
margin: 1px !important;
border: 1px solid #2a2a2a !important;
border-radius: 2px !important;
box-shadow:
inset 1px 1px 0 rgba(255, 255, 255, 0.35),
inset -1px -1px 0 rgba(0, 0, 0, 0.18) !important;
cursor: pointer !important;
}
.nitro-catalog-classic-window .layout-grid-item.has-highlight:hover {
border-color: #000 !important;
box-shadow:
inset 1px 1px 0 rgba(255, 255, 255, 0.5),
inset -1px -1px 0 rgba(0, 0, 0, 0.25),
0 0 0 1px rgba(0, 0, 0, 0.45) !important;
}
.nitro-catalog-classic-window .layout-grid-item.has-highlight.is-active {
border-color: #000 !important;
box-shadow:
inset 0 0 0 2px #ffffff,
inset 0 0 0 3px #000,
0 0 0 1px #63c5e9 !important;
}
.nitro-catalog-classic-grid-offer-icon {
position: absolute;
left: 50%;
@@ -1432,6 +1473,44 @@
right: 6px;
}
/* Bulletproof override for the rotate/state buttons. The shared SWF
button rule above lays a transparent body + border-image skin on
top, which works only when the catalog/buttons/btn_secondary*.png
sprites resolve - if they're missing the button renders 0x0
invisible. Pin the box and paint a visible gradient + outline so
the controls are always discoverable, and force z-index above the
room-previewer DIV so they sit on top of the rendered scene. */
.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn {
width: 28px !important;
height: 26px !important;
min-width: 28px !important;
min-height: 26px !important;
padding: 0 !important;
border: 1px solid #2a2a2a !important;
border-image: none !important;
border-image-source: none !important;
border-radius: 3px !important;
background: linear-gradient(180deg, #f6f6f0 0%, #d3d3c8 100%) !important;
background-color: #ecece4 !important;
background-image: linear-gradient(180deg, #f6f6f0 0%, #d3d3c8 100%) !important;
box-shadow:
inset 1px 1px 0 rgba(255, 255, 255, 0.7),
0 1px 0 rgba(0, 0, 0, 0.35) !important;
z-index: 10 !important;
}
.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn:hover {
background: linear-gradient(180deg, #ffffff 0%, #dedeD2 100%) !important;
background-image: linear-gradient(180deg, #ffffff 0%, #dedeD2 100%) !important;
border-color: #000 !important;
}
.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn:active {
background: linear-gradient(180deg, #d3d3c8 0%, #f6f6f0 100%) !important;
background-image: linear-gradient(180deg, #d3d3c8 0%, #f6f6f0 100%) !important;
box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.18) !important;
}
.nitro-catalog-classic-window .nitro-catalog-classic-navigation-shell::-webkit-scrollbar,
.nitro-catalog-classic-window .nitro-catalog-classic-navigation-list::-webkit-scrollbar,
.nitro-catalog-classic-window .nitro-catalog-classic-grid-shell::-webkit-scrollbar,
-67
View File
@@ -1,70 +1,3 @@
.nitro-swf-button {
min-height: 22px !important;
height: 22px;
padding: 2px 10px !important;
border: 3px solid transparent !important;
border-radius: 0 !important;
border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_default_9x22.png") !important;
border-image-slice: 3 3 3 3 fill !important;
border-image-width: 3px !important;
border-image-repeat: stretch !important;
background: transparent !important;
background-color: transparent !important;
background-image: none !important;
box-shadow: none !important;
color: #222 !important;
font-size: 11px !important;
font-weight: 700 !important;
line-height: 16px !important;
text-shadow: 0 1px 0 rgba(255,255,255,.75) !important;
transition: none !important;
}
.nitro-swf-button:hover {
border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_hover_9x22.png") !important;
background: transparent !important;
background-color: transparent !important;
}
.nitro-swf-button:active,
.nitro-swf-button.active {
border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_pressed_9x22.png") !important;
background: transparent !important;
background-color: transparent !important;
}
.nitro-swf-button.pointer-events-none,
.nitro-swf-button:disabled {
border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_disabled_9x22.png") !important;
color: #888 !important;
opacity: 1 !important;
}
.nitro-swf-button-success {
height: 24px;
min-height: 24px !important;
border: 6px solid transparent !important;
border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_green_24x24.png") !important;
border-image-slice: 6 6 6 6 fill !important;
border-image-width: 6px !important;
color: #fff !important;
text-shadow: 0 1px 0 rgba(0,0,0,.55) !important;
}
.nitro-swf-button-success:hover {
border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_green_hover_24x24.png") !important;
}
.nitro-swf-button-success:active,
.nitro-swf-button-success.active {
border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_green_pressed_24x24.png") !important;
}
.nitro-swf-button-success.pointer-events-none,
.nitro-swf-button-success:disabled {
border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_green_disabled_24x24.png") !important;
}
.btn-sm {
min-height: 28px;
}
-2
View File
@@ -1,6 +1,4 @@
.habbo-swf-window {
--habbo-swf-ubuntu: url("../../assets/images/catalog/swf/habbo_skin_ubuntu.png");
--habbo-swf-blue: url("../../assets/images/catalog/swf/skins/habbo_skin_blue.png");
--habbo-swf-bg: #ecece4;
--habbo-swf-panel: #f7f7f2;
--habbo-swf-border: #9d9d96;
-2
View File
@@ -46,8 +46,6 @@ import './css/inventory/InventoryView.css';
import './css/layout/LayoutTrophy.css';
import './css/navigator/HabboNavigatorDesktop.css';
import './css/nitrocard/NitroCardView.css';