From ec68204ab4e90b0b53d24bc97cff9cc7e6632bb0 Mon Sep 17 00:00:00 2001 From: Life Date: Wed, 25 Mar 2026 18:28:13 +0100 Subject: [PATCH 01/22] avatareditor fix --- src/hooks/avatar-editor/useAvatarEditor.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/hooks/avatar-editor/useAvatarEditor.ts b/src/hooks/avatar-editor/useAvatarEditor.ts index ae401de..21a1cc0 100644 --- a/src/hooks/avatar-editor/useAvatarEditor.ts +++ b/src/hooks/avatar-editor/useAvatarEditor.ts @@ -245,9 +245,12 @@ const useAvatarEditorState = () => for(let i = 0; i < MAX_PALETTES; i++) colorItems.push([]); const set = GetAvatarRenderManager().structureData.getSetType(setType); + + if(!set) return null; + const palette = GetAvatarRenderManager().structureData.getPalette(set.paletteID); - if(!set || !palette) return null; + if(!palette) return null; for(const partColor of palette.colors.getValues()) { @@ -287,10 +290,10 @@ const useAvatarEditorState = () => return { setType, partItems, colorItems }; }; - newAvatarModels[AvatarEditorFigureCategory.GENERIC] = [ AvatarFigurePartType.HEAD ].map(setType => buildCategory(setType)); - newAvatarModels[AvatarEditorFigureCategory.HEAD] = [ AvatarFigurePartType.HAIR, AvatarFigurePartType.HEAD_ACCESSORY, AvatarFigurePartType.HEAD_ACCESSORY_EXTRA, AvatarFigurePartType.EYE_ACCESSORY, AvatarFigurePartType.FACE_ACCESSORY ].map(setType => buildCategory(setType)); - newAvatarModels[AvatarEditorFigureCategory.TORSO] = [ AvatarFigurePartType.CHEST, AvatarFigurePartType.CHEST_PRINT, AvatarFigurePartType.COAT_CHEST, AvatarFigurePartType.CHEST_ACCESSORY ].map(setType => buildCategory(setType)); - newAvatarModels[AvatarEditorFigureCategory.LEGS] = [ AvatarFigurePartType.LEGS, AvatarFigurePartType.SHOES, AvatarFigurePartType.WAIST_ACCESSORY ].map(setType => buildCategory(setType)); + newAvatarModels[AvatarEditorFigureCategory.GENERIC] = [ AvatarFigurePartType.HEAD ].map(setType => buildCategory(setType)).filter(Boolean); + newAvatarModels[AvatarEditorFigureCategory.HEAD] = [ AvatarFigurePartType.HAIR, AvatarFigurePartType.HEAD_ACCESSORY, AvatarFigurePartType.HEAD_ACCESSORY_EXTRA, AvatarFigurePartType.EYE_ACCESSORY, AvatarFigurePartType.FACE_ACCESSORY ].map(setType => buildCategory(setType)).filter(Boolean); + newAvatarModels[AvatarEditorFigureCategory.TORSO] = [ AvatarFigurePartType.CHEST, AvatarFigurePartType.CHEST_PRINT, AvatarFigurePartType.COAT_CHEST, AvatarFigurePartType.CHEST_ACCESSORY ].map(setType => buildCategory(setType)).filter(Boolean); + newAvatarModels[AvatarEditorFigureCategory.LEGS] = [ AvatarFigurePartType.LEGS, AvatarFigurePartType.SHOES, AvatarFigurePartType.WAIST_ACCESSORY ].map(setType => buildCategory(setType)).filter(Boolean); newAvatarModels[AvatarEditorFigureCategory.PETS] = [ AvatarFigurePartType.PET ].map(setType => buildCategory(setType)).filter(Boolean); newAvatarModels[AvatarEditorFigureCategory.WARDROBE] = []; From a095d818a36c4b43ead29d59ec9df7338de9b02d Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 27 Mar 2026 09:53:55 +0100 Subject: [PATCH 02/22] =?UTF-8?q?=F0=9F=86=99=20Fix=20move=20avatar-editor?= =?UTF-8?q?=20to=20Tailwind?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../avatar-editor/AvatarEditorIcon.tsx | 90 +++++--- src/css/index.css | 216 +----------------- 2 files changed, 65 insertions(+), 241 deletions(-) diff --git a/src/components/avatar-editor/AvatarEditorIcon.tsx b/src/components/avatar-editor/AvatarEditorIcon.tsx index f5623ed..e992373 100644 --- a/src/components/avatar-editor/AvatarEditorIcon.tsx +++ b/src/components/avatar-editor/AvatarEditorIcon.tsx @@ -1,45 +1,81 @@ import { DetailedHTMLProps, HTMLAttributes, PropsWithChildren, forwardRef } from 'react'; import { classNames } from '../../layout'; -type AvatarIconType = 'male' | 'female' | 'clear' | 'sellable'; +import arrowLeftIcon from '../../assets/images/avatareditor/arrow-left-icon.png'; +import arrowRightIcon from '../../assets/images/avatareditor/arrow-right-icon.png'; +import caIcon from '../../assets/images/avatareditor/ca-icon.png'; +import caSelectedIcon from '../../assets/images/avatareditor/ca-selected-icon.png'; +import ccIcon from '../../assets/images/avatareditor/cc-icon.png'; +import ccSelectedIcon from '../../assets/images/avatareditor/cc-selected-icon.png'; +import chIcon from '../../assets/images/avatareditor/ch-icon.png'; +import chSelectedIcon from '../../assets/images/avatareditor/ch-selected-icon.png'; +import clearIcon from '../../assets/images/avatareditor/clear-icon.png'; +import cpIcon from '../../assets/images/avatareditor/cp-icon.png'; +import cpSelectedIcon from '../../assets/images/avatareditor/cp-selected-icon.png'; +import eaIcon from '../../assets/images/avatareditor/ea-icon.png'; +import eaSelectedIcon from '../../assets/images/avatareditor/ea-selected-icon.png'; +import faIcon from '../../assets/images/avatareditor/fa-icon.png'; +import faSelectedIcon from '../../assets/images/avatareditor/fa-selected-icon.png'; +import femaleIcon from '../../assets/images/avatareditor/female-icon.png'; +import femaleSelectedIcon from '../../assets/images/avatareditor/female-selected-icon.png'; +import haIcon from '../../assets/images/avatareditor/ha-icon.png'; +import haSelectedIcon from '../../assets/images/avatareditor/ha-selected-icon.png'; +import heIcon from '../../assets/images/avatareditor/he-icon.png'; +import heSelectedIcon from '../../assets/images/avatareditor/he-selected-icon.png'; +import hrIcon from '../../assets/images/avatareditor/hr-icon.png'; +import hrSelectedIcon from '../../assets/images/avatareditor/hr-selected-icon.png'; +import lgIcon from '../../assets/images/avatareditor/lg-icon.png'; +import lgSelectedIcon from '../../assets/images/avatareditor/lg-selected-icon.png'; +import maleIcon from '../../assets/images/avatareditor/male-icon.png'; +import maleSelectedIcon from '../../assets/images/avatareditor/male-selected-icon.png'; +import sellableIcon from '../../assets/images/avatareditor/sellable-icon.png'; +import shIcon from '../../assets/images/avatareditor/sh-icon.png'; +import shSelectedIcon from '../../assets/images/avatareditor/sh-selected-icon.png'; +import waIcon from '../../assets/images/avatareditor/wa-icon.png'; +import waSelectedIcon from '../../assets/images/avatareditor/wa-selected-icon.png'; + +const ICON_MAP: Record = { + 'arrow-left': { normal: arrowLeftIcon }, + 'arrow-right': { normal: arrowRightIcon }, + 'ca': { normal: caIcon, selected: caSelectedIcon }, + 'cc': { normal: ccIcon, selected: ccSelectedIcon }, + 'ch': { normal: chIcon, selected: chSelectedIcon }, + 'clear': { normal: clearIcon }, + 'cp': { normal: cpIcon, selected: cpSelectedIcon }, + 'ea': { normal: eaIcon, selected: eaSelectedIcon }, + 'fa': { normal: faIcon, selected: faSelectedIcon }, + 'female': { normal: femaleIcon, selected: femaleSelectedIcon }, + 'ha': { normal: haIcon, selected: haSelectedIcon }, + 'he': { normal: heIcon, selected: heSelectedIcon }, + 'hr': { normal: hrIcon, selected: hrSelectedIcon }, + 'lg': { normal: lgIcon, selected: lgSelectedIcon }, + 'male': { normal: maleIcon, selected: maleSelectedIcon }, + 'sellable': { normal: sellableIcon }, + 'sh': { normal: shIcon, selected: shSelectedIcon }, + 'wa': { normal: waIcon, selected: waSelectedIcon }, +}; export const AvatarEditorIcon = forwardRef & DetailedHTMLProps, HTMLDivElement>>((props, ref) => { - const { icon = null, selected = false, className = null, ...rest } = props; + const { icon = null, selected = false, className = null, children, ...rest } = props; - /* - switch (icon) - { - case 'male': + const iconEntry = icon ? ICON_MAP[icon] : null; + if(!iconEntry) return null; - break; + const src = (selected && iconEntry.selected) ? iconEntry.selected : iconEntry.normal; - case 'arrow-left': - - break; - - default: - //statements; - break; - - } -*/ return (
+ className={ classNames('flex items-center justify-center cursor-pointer', className) } + { ...rest }> + { + { children } +
); }); diff --git a/src/css/index.css b/src/css/index.css index 6c62fc7..fbfc8a4 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -447,219 +447,7 @@ body { } } -.nitro-avatar-editor-spritesheet { - background: url('@/assets/images/avatareditor/avatar-editor-spritesheet.png') transparent no-repeat; - - &.arrow-left-icon { - width: 28px; - height: 21px; - background-position: -226px -131px; - } - - &.arrow-right-icon { - width: 28px; - height: 21px; - background-position: -226px -162px; - } - - &.ca-icon { - width: 25px; - height: 25px; - background-position: -226px -61px; - - &.selected { - width: 25px; - height: 25px; - background-position: -226px -96px; - } - } - - &.cc-icon { - width: 31px; - height: 29px; - background-position: -145px -5px; - - &.selected { - width: 31px; - height: 29px; - background-position: -145px -44px; - } - } - - &.ch-icon { - width: 29px; - height: 24px; - background-position: -186px -39px; - - &.selected { - width: 29px; - height: 24px; - background-position: -186px -73px; - } - } - - &.clear-icon { - width: 27px; - height: 27px; - background-position: -145px -157px; - } - - &.cp-icon { - width: 30px; - height: 24px; - background-position: -145px -264px; - - &.selected { - width: 30px; - height: 24px; - background-position: -186px -5px; - } - } - - - &.ea-icon { - width: 35px; - height: 16px; - background-position: -226px -193px; - - &.selected { - width: 35px; - height: 16px; - background-position: -226px -219px; - } - } - - &.fa-icon { - width: 27px; - height: 20px; - background-position: -186px -137px; - - &.selected { - width: 27px; - height: 20px; - background-position: -186px -107px; - } - } - - &.female-icon { - width: 18px; - height: 27px; - background-position: -186px -202px; - - &.selected { - width: 18px; - height: 27px; - background-position: -186px -239px; - } - } - - &.ha-icon { - width: 25px; - height: 22px; - background-position: -226px -245px; - - &.selected { - width: 25px; - height: 22px; - background-position: -226px -277px; - } - } - - &.he-icon { - width: 31px; - height: 27px; - background-position: -145px -83px; - - &.selected { - width: 31px; - height: 27px; - background-position: -145px -120px; - } - } - - &.hr-icon { - width: 29px; - height: 25px; - background-position: -145px -194px; - - &.selected { - width: 29px; - height: 25px; - background-position: -145px -229px; - } - } - - &.lg-icon { - width: 19px; - height: 20px; - background-position: -303px -45px; - - &.selected { - width: 19px; - height: 20px; - background-position: -303px -75px; - } - } - - &.loading-icon { - width: 21px; - height: 25px; - background-position: -186px -167px; - } - - - &.male-icon { - width: 21px; - height: 21px; - background-position: -186px -276px; - - &.selected { - width: 21px; - height: 21px; - background-position: -272px -5px; - } - } - - - &.sellable-icon { - width: 17px; - height: 15px; - background-position: -303px -105px; - } - - - &.sh-icon { - width: 37px; - height: 10px; - background-position: -303px -5px; - - &.selected { - width: 37px; - height: 10px; - background-position: -303px -25px; - } - } - - - &.spotlight-icon { - width: 130px; - height: 305px; - background-position: -5px -5px; - } - - - &.wa-icon { - width: 36px; - height: 18px; - background-position: -226px -5px; - - &.selected { - width: 36px; - height: 18px; - background-position: -226px -33px; - } - } -} +/* Avatar editor icons are now rendered as tags via AvatarEditorIcon.tsx */ .nitro-avatar-editor-wardrobe-figure-preview { background-color: #677181; @@ -710,7 +498,7 @@ body { .category-item { - height: 40px; + height: 32px; } .figure-preview-container { From a4d476410545987f45fd951eaa732d4a9b182681 Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 27 Mar 2026 10:08:02 +0100 Subject: [PATCH 03/22] =?UTF-8?q?=F0=9F=86=99=20Moved=20catalogue=20view?= =?UTF-8?q?=20to=20Tailwind?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/catalog/CatalogModernView.tsx | 4 +- .../views/catalog-icon/CatalogIconView.tsx | 8 +-- .../catalog-rail/CatalogRailItemView.tsx | 4 +- .../views/favorites/CatalogFavoritesView.tsx | 2 +- .../navigation/CatalogNavigationItemView.tsx | 2 +- src/css/index.css | 50 ++++++++----------- 6 files changed, 32 insertions(+), 38 deletions(-) diff --git a/src/components/catalog/CatalogModernView.tsx b/src/components/catalog/CatalogModernView.tsx index ec19b70..95368b0 100644 --- a/src/components/catalog/CatalogModernView.tsx +++ b/src/components/catalog/CatalogModernView.tsx @@ -111,7 +111,7 @@ const CatalogModernViewInner: FC<{}> = () => className={ `flex items-center gap-2 mx-1 px-1.5 py-1.5 rounded cursor-pointer transition-all duration-150 ${ showFavorites ? 'bg-primary text-white' : 'hover:bg-card-grid-item-active' }` } onClick={ () => setShowFavorites(!showFavorites) } > -
+
0 ? 'text-danger' : 'text-muted' }` } /> { totalFavs > 0 && @@ -163,7 +163,7 @@ const CatalogModernViewInner: FC<{}> = () => activateNode(child); } } > -
+
{ isHidden && }
diff --git a/src/components/catalog/views/catalog-icon/CatalogIconView.tsx b/src/components/catalog/views/catalog-icon/CatalogIconView.tsx index 0178662..4e5ed5a 100644 --- a/src/components/catalog/views/catalog-icon/CatalogIconView.tsx +++ b/src/components/catalog/views/catalog-icon/CatalogIconView.tsx @@ -1,20 +1,20 @@ import { FC, useMemo } from 'react'; import { GetConfigurationValue } from '../../../../api'; -import { LayoutImage } from '../../../../common'; export interface CatalogIconViewProps { icon: number; + className?: string; } export const CatalogIconView: FC = props => { - const { icon = 0 } = props; + const { icon = 0, className = '' } = props; - const getIconUrl = useMemo(() => + const iconUrl = useMemo(() => { return ((GetConfigurationValue('catalog.asset.icon.url')).replace('%name%', icon.toString())); }, [ icon ]); - return ; + return ; }; diff --git a/src/components/catalog/views/catalog-rail/CatalogRailItemView.tsx b/src/components/catalog/views/catalog-rail/CatalogRailItemView.tsx index 3b1cd21..4d1e38e 100644 --- a/src/components/catalog/views/catalog-rail/CatalogRailItemView.tsx +++ b/src/components/catalog/views/catalog-rail/CatalogRailItemView.tsx @@ -19,8 +19,8 @@ export const CatalogRailItemView: FC = props => title={ node.localization } onClick={ onClick } > -
- +
+
{ node.localization } diff --git a/src/components/catalog/views/favorites/CatalogFavoritesView.tsx b/src/components/catalog/views/favorites/CatalogFavoritesView.tsx index 676f771..77b02e6 100644 --- a/src/components/catalog/views/favorites/CatalogFavoritesView.tsx +++ b/src/components/catalog/views/favorites/CatalogFavoritesView.tsx @@ -121,7 +121,7 @@ export const CatalogFavoritesView: FC = props => onClick={ () => { openPageByOfferId(fav.offerId); onClose(); } } > { /* Furni icon */ } -
+
{ fav.iconUrl ? : fav.nodeIconId !== null diff --git a/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx b/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx index d31d801..f897cad 100644 --- a/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx +++ b/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx @@ -86,7 +86,7 @@ export const CatalogNavigationItemView: FC = pro > { adminMode && } -
+
{ node.localization } diff --git a/src/css/index.css b/src/css/index.css index fbfc8a4..709acdb 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -30,51 +30,45 @@ body { } ::-webkit-scrollbar { - width: .5rem + width: .625rem; } ::-webkit-scrollbar:horizontal { - height: .5rem + height: .625rem; } ::-webkit-scrollbar:not(:horizontal) { - width: .5rem + width: .625rem; } -::-webkit-scrollbar-track:horizontal { - border-bottom: .25rem solid rgba(0, 0, 0, .1) +::-webkit-scrollbar-track { + background: rgba(0, 0, 0, .08); + border-radius: .5rem; } -::-webkit-scrollbar-track:not(:horizontal) { - border-right: .25rem solid rgba(0, 0, 0, .1) +::-webkit-scrollbar-thumb { + background: rgba(30, 114, 149, .35); + border-radius: .5rem; + border: 2px solid transparent; + background-clip: padding-box; } -::-webkit-scrollbar-thumb:horizontal { - border-bottom: .25rem solid rgba(30, 114, 149, .4) +::-webkit-scrollbar-thumb:hover { + background: rgba(30, 114, 149, .6); + border-radius: .5rem; + border: 2px solid transparent; + background-clip: padding-box; } -::-webkit-scrollbar-thumb:horizontal:hover { - border-bottom: .25rem solid rgba(30, 114, 149, .8) -} - -::-webkit-scrollbar-thumb:horizontal:active { - border-bottom: .25rem solid #185D79 -} - -::-webkit-scrollbar-thumb:not(:horizontal) { - border-right: .25rem solid rgba(30, 114, 149, .4) -} - -::-webkit-scrollbar-thumb:not(:horizontal):hover { - border-right: .25rem solid rgba(30, 114, 149, .8) -} - -::-webkit-scrollbar-thumb:not(:horizontal):active { - border-right: .25rem solid #185D79 +::-webkit-scrollbar-thumb:active { + background: #185D79; + border-radius: .5rem; + border: 2px solid transparent; + background-clip: padding-box; } ::-webkit-scrollbar-corner { - background: rgba(0, 0, 0, .1) + background: rgba(0, 0, 0, .08); } @layer components { From bbe71e9753ce00b68c57216f18a2cf9df5ccbb8c Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 27 Mar 2026 13:38:03 +0100 Subject: [PATCH 04/22] =?UTF-8?q?=F0=9F=86=99=20Cleanup=20Furni-Edit=20&?= =?UTF-8?q?=20Fix=20the=20avatar-editor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../avatar/AvatarEditorThumbnailsHelper.ts | 20 +- src/components/MainView.tsx | 2 - .../AvatarEditorFigureSetItemView.tsx | 28 +- .../figure-set/AvatarEditorFigureSetView.tsx | 5 +- .../infostand/InfoStandWidgetFurniView.tsx | 13 - src/components/toolbar/ToolbarView.tsx | 2 - src/hooks/furni-editor/index.ts | 1 - src/hooks/furni-editor/useFurniEditor.ts | 239 ------------------ 8 files changed, 27 insertions(+), 283 deletions(-) delete mode 100644 src/hooks/furni-editor/index.ts delete mode 100644 src/hooks/furni-editor/useFurniEditor.ts diff --git a/src/api/avatar/AvatarEditorThumbnailsHelper.ts b/src/api/avatar/AvatarEditorThumbnailsHelper.ts index f9ce18f..bffc418 100644 --- a/src/api/avatar/AvatarEditorThumbnailsHelper.ts +++ b/src/api/avatar/AvatarEditorThumbnailsHelper.ts @@ -109,22 +109,6 @@ export class AvatarEditorThumbnailsHelper container.addChild(sprite); } - if(container.children.length > 0) - { - const isPetPart = parts.some(p => p.type === 'pt' || p.type === 'ptl' || p.type === 'ptr'); - - if(isPetPart) - { - const bounds = container.getBounds(); - - for(const child of container.children) - { - (child as NitroSprite).position.x -= bounds.x; - (child as NitroSprite).position.y -= bounds.y; - } - } - } - return container; }; @@ -133,9 +117,9 @@ export class AvatarEditorThumbnailsHelper const resetFigure = async (figure: string) => { const container = buildContainer(part, useColors, partColors, isDisabled); - const imageUrl = await TextureUtils.generateImageUrl(container); + const imageUrl = await TextureUtils.generateImageUrl({ target: container, resolution: 1 }); - AvatarEditorThumbnailsHelper.THUMBNAIL_CACHE.set(thumbnailKey, imageUrl); + if(imageUrl) AvatarEditorThumbnailsHelper.THUMBNAIL_CACHE.set(thumbnailKey, imageUrl); resolve(imageUrl); }; diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 4a39a59..b03d21d 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -9,7 +9,6 @@ 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'; @@ -121,7 +120,6 @@ export const MainView: FC<{}> = props => - diff --git a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx index a08321d..789dc74 100644 --- a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx +++ b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx @@ -1,4 +1,3 @@ -import { AvatarEditorFigureCategory, AvatarFigurePartType } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; import { AvatarEditorThumbnailsHelper, GetClubMemberLevel, GetConfigurationValue, IAvatarEditorCategoryPartItem } from '../../../api'; import { LayoutCurrencyIcon, LayoutGridItemProps } from '../../../common'; @@ -15,7 +14,7 @@ export const AvatarEditorFigureSetItemView: FC<{ { const { setType = null, partItem = null, isSelected = false, width = '100%', ...rest } = props; const [ assetUrl, setAssetUrl ] = useState(''); - const { activeModelKey = '', selectedColorParts = null, getFigureStringWithFace = null } = useAvatarEditor(); + const { selectedColorParts = null, getFigureStringWithFace = null } = useAvatarEditor(); const clubLevel = partItem.partSet?.clubLevel ?? 0; const isHC = !GetConfigurationValue('hc.disabled', false) && (clubLevel > 0); @@ -24,7 +23,9 @@ export const AvatarEditorFigureSetItemView: FC<{ useEffect(() => { - if(!setType || !setType.length || !partItem) return; + setAssetUrl(''); + + if(!setType || !setType.length || !partItem || partItem.isClear) return; const loadImage = async () => { @@ -34,7 +35,7 @@ export const AvatarEditorFigureSetItemView: FC<{ let url: string = null; - if(setType === AvatarFigurePartType.HEAD && activeModelKey !== AvatarEditorFigureCategory.NFT) + if(setType === 'hd') { url = await AvatarEditorThumbnailsHelper.buildForFace(getFigureStringWithFace(partItem.id), partIsLocked || isSellableNotOwned); } @@ -53,12 +54,27 @@ export const AvatarEditorFigureSetItemView: FC<{ }; loadImage(); - }, [ setType, partItem, selectedColorParts, getFigureStringWithFace, isSellableNotOwned, activeModelKey ]); + }, [ setType, partItem, selectedColorParts, getFigureStringWithFace, isSellableNotOwned ]); if(!partItem) return null; + const isHead = (setType === 'hd'); + return ( - + + { !partItem.isClear && assetUrl && !isHead && + } { !partItem.isClear && isHC && } { partItem.isClear && } { !partItem.isClear && partItem.partSet.isSellable && !isSellableNotOwned && } diff --git a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetView.tsx b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetView.tsx index 7d7bd68..179f894 100644 --- a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetView.tsx +++ b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetView.tsx @@ -7,9 +7,10 @@ import { AvatarEditorFigureSetItemView } from './AvatarEditorFigureSetItemView'; export const AvatarEditorFigureSetView: FC<{ category: IAvatarEditorCategory; columnCount: number; + estimateSize?: number; }> = props => { - const { category = null, columnCount = 3 } = props; + const { category = null, columnCount = 3, estimateSize = 50 } = props; const { selectedParts = null, selectEditorPart } = useAvatarEditor(); const isPartItemSelected = (partItem: IAvatarEditorCategoryPartItem) => @@ -29,7 +30,7 @@ export const AvatarEditorFigureSetView: FC<{ }; return ( - columnCount={ columnCount } estimateSize={ 50 } itemRender={ (item: IAvatarEditorCategoryPartItem) => + columnCount={ columnCount } estimateSize={ estimateSize } itemRender={ (item: IAvatarEditorCategoryPartItem) => { if(!item) return null; diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx index 48e9b84..98604a6 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx @@ -574,19 +574,6 @@ export const InfoStandWidgetFurniView: FC = props onClick={ () => setDropdownOpen(!dropdownOpen) }> { dropdownOpen ? `${LocalizeText('widget.furni.present.close')} Buildtools` : `${LocalizeText('navigator.roomsettings.doormode.open')} Buildtools` } - { dropdownOpen &&
{ /* Left panel: position + rotation */ } diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index fbb5ae8..ea9fb16 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -96,8 +96,6 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => CreateLinkEvent('camera/toggle') } /> } { isMod && CreateLinkEvent('mod-tools/toggle') } /> } - { isMod && - CreateLinkEvent('furni-editor/toggle') } /> } diff --git a/src/hooks/furni-editor/index.ts b/src/hooks/furni-editor/index.ts deleted file mode 100644 index 47ce6ef..0000000 --- a/src/hooks/furni-editor/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useFurniEditor'; diff --git a/src/hooks/furni-editor/useFurniEditor.ts b/src/hooks/furni-editor/useFurniEditor.ts deleted file mode 100644 index e4258e5..0000000 --- a/src/hooks/furni-editor/useFurniEditor.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { useCallback, useState } from 'react'; - -export interface FurniItem -{ - id: number; - spriteId: number; - itemName: string; - publicName: string; - type: string; - width: number; - length: number; - stackHeight: number; - allowStack: boolean; - allowWalk: boolean; - allowSit: boolean; - allowLay: boolean; - interactionType: string; - interactionModesCount: number; -} - -export interface FurniDetail extends FurniItem -{ - allowGift: boolean; - allowTrade: boolean; - allowRecycle: boolean; - allowMarketplaceSell: boolean; - allowInventoryStack: boolean; - vendingIds: string; - customparams: string; - effectIdMale: number; - effectIdFemale: number; - clothingOnWalk: string; - multiheight: string; - description: string; - usageCount: number; -} - -export interface CatalogRef -{ - id: number; - catalogName: string; - costCredits: number; - costPoints: number; - pointsType: number; - pageId: number; - pageName: string; -} - -const API_BASE = '/api/admin/furni-editor'; - -async function apiFetch(url: string, options?: RequestInit): Promise -{ - const res = await fetch(url, { credentials: 'include', ...options }); - const data = await res.json(); - - if(!res.ok || data.error) throw new Error(data.error || 'API error'); - - return data; -} - -export const useFurniEditor = () => -{ - const [ items, setItems ] = useState([]); - const [ total, setTotal ] = useState(0); - const [ page, setPage ] = useState(1); - const [ loading, setLoading ] = useState(false); - const [ error, setError ] = useState(null); - const [ selectedItem, setSelectedItem ] = useState(null); - const [ catalogItems, setCatalogItems ] = useState([]); - const [ interactions, setInteractions ] = useState([]); - const [ furniDataEntry, setFurniDataEntry ] = useState | null>(null); - - const clearError = useCallback(() => setError(null), []); - - const searchItems = useCallback(async (query: string, type: string, pg: number) => - { - setLoading(true); - setError(null); - - try - { - const params = new URLSearchParams({ q: query, limit: '20', page: String(pg) }); - - if(type) params.set('type', type); - - const data = await apiFetch<{ items: FurniItem[]; total: number; page: number }>(`${ API_BASE }?${ params }`); - - setItems(data.items); - setTotal(data.total); - setPage(data.page); - } - catch(e: any) - { - setError(e.message); - } - finally - { - setLoading(false); - } - }, []); - - const loadDetail = useCallback(async (id: number): Promise => - { - setLoading(true); - setError(null); - - try - { - const data = await apiFetch<{ item: FurniDetail; catalogItems: CatalogRef[]; furniDataEntry: Record | null }>(`${ API_BASE }/detail?id=${ id }`); - - setSelectedItem(data.item); - setCatalogItems(data.catalogItems); - setFurniDataEntry(data.furniDataEntry); - - return true; - } - catch(e: any) - { - setError(e.message); - - return false; - } - finally - { - setLoading(false); - } - }, []); - - const updateItem = useCallback(async (id: number, fields: Record) => - { - setLoading(true); - setError(null); - - try - { - await apiFetch(`${ API_BASE }/update?id=${ id }`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(fields) - }); - - return true; - } - catch(e: any) - { - setError(e.message); - - return false; - } - finally - { - setLoading(false); - } - }, []); - - const createItem = useCallback(async (fields: Record) => - { - setLoading(true); - setError(null); - - try - { - const data = await apiFetch<{ id: number }>(`${ API_BASE }`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(fields) - }); - - return data.id; - } - catch(e: any) - { - setError(e.message); - - return null; - } - finally - { - setLoading(false); - } - }, []); - - const deleteItem = useCallback(async (id: number) => - { - setLoading(true); - setError(null); - - try - { - await apiFetch(`${ API_BASE }/delete?id=${ id }`, { method: 'POST' }); - - return true; - } - catch(e: any) - { - setError(e.message); - - return false; - } - finally - { - setLoading(false); - } - }, []); - - const loadInteractions = useCallback(async () => - { - try - { - const data = await apiFetch<{ interactions: Array }>(`${ API_BASE }/interactions`); - - setInteractions(data.interactions.map(i => typeof i === 'string' ? i : i.name)); - } - catch {} - }, []); - - const loadBySpriteId = useCallback(async (spriteId: number): Promise => - { - try - { - const data = await apiFetch<{ id: number }>(`${ API_BASE }/by-sprite?spriteId=${ spriteId }`); - - return await loadDetail(data.id); - } - catch(e: any) - { - setError(e.message); - - return false; - } - }, [ loadDetail ]); - - return { - items, total, page, loading, error, clearError, - selectedItem, setSelectedItem, catalogItems, furniDataEntry, - interactions, - searchItems, loadDetail, loadBySpriteId, updateItem, createItem, deleteItem, loadInteractions - }; -}; From a959bdce0495f91b9afa2704d2e65e3c2b5c7c81 Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Sat, 28 Mar 2026 08:36:11 +0100 Subject: [PATCH 05/22] =?UTF-8?q?=E2=AD=90=20Start=20the=20forum=20framewo?= =?UTF-8?q?rk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/MainView.tsx | 2 + .../groups/views/GroupInformationView.tsx | 5 + .../views/forums/GroupForumListView.tsx | 114 +++++++ .../views/forums/GroupForumNewThreadView.tsx | 95 ++++++ .../views/forums/GroupForumSettingsView.tsx | 113 +++++++ .../views/forums/GroupForumThreadListView.tsx | 187 ++++++++++++ .../views/forums/GroupForumThreadView.tsx | 286 ++++++++++++++++++ .../groups/views/forums/GroupForumView.tsx | 167 ++++++++++ src/components/groups/views/forums/index.ts | 6 + src/components/toolbar/ToolbarMeView.tsx | 3 +- src/css/index.css | 10 +- src/layout/InfiniteGrid.tsx | 7 +- 12 files changed, 988 insertions(+), 7 deletions(-) create mode 100644 src/components/groups/views/forums/GroupForumListView.tsx create mode 100644 src/components/groups/views/forums/GroupForumNewThreadView.tsx create mode 100644 src/components/groups/views/forums/GroupForumSettingsView.tsx create mode 100644 src/components/groups/views/forums/GroupForumThreadListView.tsx create mode 100644 src/components/groups/views/forums/GroupForumThreadView.tsx create mode 100644 src/components/groups/views/forums/GroupForumView.tsx create mode 100644 src/components/groups/views/forums/index.ts diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index b03d21d..b1660a5 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -12,6 +12,7 @@ import { FloorplanEditorView } from './floorplan-editor/FloorplanEditorView'; import { FriendsView } from './friends/FriendsView'; import { GameCenterView } from './game-center/GameCenterView'; import { GroupsView } from './groups/GroupsView'; +import { GroupForumView } from './groups/views/forums/GroupForumView'; import { GuideToolView } from './guide-tool/GuideToolView'; import { HcCenterView } from './hc-center/HcCenterView'; import { HelpView } from './help/HelpView'; @@ -112,6 +113,7 @@ export const MainView: FC<{}> = props => + diff --git a/src/components/groups/views/GroupInformationView.tsx b/src/components/groups/views/GroupInformationView.tsx index d2f8a80..5da2719 100644 --- a/src/components/groups/views/GroupInformationView.tsx +++ b/src/components/groups/views/GroupInformationView.tsx @@ -94,6 +94,9 @@ export const GroupInformationView: FC = props => case 'popular_groups': CreateLinkEvent('navigator/search/groups'); break; + case 'forum': + CreateLinkEvent('groupforum/' + groupInformation.id); + break; } }; @@ -134,6 +137,8 @@ export const GroupInformationView: FC = props => handleAction('homeroom') }>{ LocalizeText('group.linktobase') } handleAction('furniture') }>{ LocalizeText('group.buyfurni') } handleAction('popular_groups') }>{ LocalizeText('group.showgroups') } + { groupInformation.hasForum && + handleAction('forum') }>{ LocalizeText('group.showforum') } }
{ (groupInformation.type !== GroupType.PRIVATE || groupInformation.type === GroupType.PRIVATE && groupInformation.membershipType === GroupMembershipType.MEMBER) &&