From 3cf8c9b89a5f1699a5d064047a14c3c4b3290bcf Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 19 Feb 2026 13:22:37 +0100 Subject: [PATCH] :up: Added delete Furni / Pets in inventory --- src/components/index.scss | 1 - src/components/inventory/InventoryView.tsx | 109 ++++++++++------- .../views/InventoryCategoryFilterView.tsx | 114 ++++++++++++++++++ .../views/badge/InventoryBadgeView.tsx | 9 +- .../InventoryFurnitureDeleteView.tsx | 111 +++++++++++++++++ .../furniture/InventoryFurnitureView.tsx | 56 ++++++--- .../inventory/views/pet/InventoryPetView.tsx | 28 ++++- src/css/inventory/Inventory.css | 29 +++++ .../inventory/DeleteItemConfirmEvent.ts | 14 +++ src/events/inventory/index.ts | 1 + src/index.tsx | 2 + 11 files changed, 403 insertions(+), 71 deletions(-) create mode 100644 src/components/inventory/views/InventoryCategoryFilterView.tsx create mode 100644 src/components/inventory/views/furniture/InventoryFurnitureDeleteView.tsx create mode 100644 src/css/inventory/Inventory.css create mode 100644 src/events/inventory/DeleteItemConfirmEvent.ts diff --git a/src/components/index.scss b/src/components/index.scss index 3b52e97..8be9445 100644 --- a/src/components/index.scss +++ b/src/components/index.scss @@ -12,7 +12,6 @@ @import './hc-center/HcCenterView'; @import './help/HelpView'; @import './hotel-view/HotelView'; -@import './inventory/InventoryView'; @import './loading/LoadingView'; @import './mod-tools/ModToolsView'; @import './navigator/NavigatorView'; diff --git a/src/components/inventory/InventoryView.tsx b/src/components/inventory/InventoryView.tsx index 319c794..8f2dde8 100644 --- a/src/components/inventory/InventoryView.tsx +++ b/src/components/inventory/InventoryView.tsx @@ -1,10 +1,12 @@ -import { NitroCard } from '@layout/NitroCard'; import { AddLinkEventTracker, BadgePointLimitsEvent, GetLocalizationManager, GetRoomEngine, ILinkEventTracker, IRoomSession, RemoveLinkEventTracker, RoomEngineObjectEvent, RoomEngineObjectPlacedEvent, RoomPreviewer, RoomSessionEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { LocalizeText, UnseenItemCategory, isObjectMoverRequested, setObjectMoverRequested } from '../../api'; -import { useInventoryTrade, useInventoryUnseenTracker, useMessageEvent, useNitroEvent } from '../../hooks'; +import { GroupItem, LocalizeText, UnseenItemCategory, isObjectMoverRequested, setObjectMoverRequested } from '../../api'; +import { NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; +import { useInventoryBadges, useInventoryFurni, useInventoryTrade, useInventoryUnseenTracker, useMessageEvent, useNitroEvent } from '../../hooks'; +import { InventoryCategoryFilterView } from './views/InventoryCategoryFilterView'; import { InventoryBadgeView } from './views/badge/InventoryBadgeView'; import { InventoryBotView } from './views/bot/InventoryBotView'; +import { InventoryFurnitureDeleteView } from './views/furniture/InventoryFurnitureDeleteView'; import { InventoryFurnitureView } from './views/furniture/InventoryFurnitureView'; import { InventoryTradeView } from './views/furniture/InventoryTradeView'; import { InventoryPetView } from './views/pet/InventoryPetView'; @@ -13,8 +15,8 @@ const TAB_FURNITURE: string = 'inventory.furni'; const TAB_BOTS: string = 'inventory.bots'; const TAB_PETS: string = 'inventory.furni.tab.pets'; const TAB_BADGES: string = 'inventory.badges'; -const TABS = [ TAB_FURNITURE, TAB_BOTS, TAB_PETS, TAB_BADGES ]; -const UNSEEN_CATEGORIES = [ UnseenItemCategory.FURNI, UnseenItemCategory.BOT, UnseenItemCategory.PET, UnseenItemCategory.BADGE ]; +const TABS = [ TAB_FURNITURE, TAB_PETS, TAB_BADGES, TAB_BOTS ]; +const UNSEEN_CATEGORIES = [ UnseenItemCategory.FURNI, UnseenItemCategory.PET, UnseenItemCategory.BADGE, UnseenItemCategory.BOT ]; export const InventoryView: FC<{}> = props => { @@ -22,8 +24,12 @@ export const InventoryView: FC<{}> = props => const [ currentTab, setCurrentTab ] = useState(TABS[0]); const [ roomSession, setRoomSession ] = useState(null); const [ roomPreviewer, setRoomPreviewer ] = useState(null); + const [ filteredGroupItems, setFilteredGroupItems ] = useState([]); + const [ filteredBadgeCodes, setFilteredBadgeCodes ] = useState([]); const { isTrading = false, stopTrading = null } = useInventoryTrade(); - const { getCount = null, resetCategory = null } = useInventoryUnseenTracker(); + const { getCount = null } = useInventoryUnseenTracker(); + const { groupItems = [] } = useInventoryFurni(); + const { badgeCodes = [] } = useInventoryBadges(); const onClose = () => { @@ -117,44 +123,59 @@ export const InventoryView: FC<{}> = props => if(!isVisible) return null; + const showFilter = !isTrading && (currentTab === TAB_FURNITURE || currentTab === TAB_BADGES); + return ( - - - { !isTrading && - <> - - { TABS.map((name, index) => - { - return ( - setCurrentTab(name) }> - { LocalizeText(name) } - - ); - }) } - - - { (currentTab === TAB_FURNITURE ) && - } - { (currentTab === TAB_BOTS ) && - } - { (currentTab === TAB_PETS ) && - } - { (currentTab === TAB_BADGES ) && - } - - } - { isTrading && - - - } - + <> + + + { !isTrading && + <> + + { TABS.map((name, index) => + { + return ( + setCurrentTab(name) }> + { LocalizeText(name) } + + ); + }) } + +
+ { showFilter && + } +
+ { (currentTab === TAB_FURNITURE) && + } + { (currentTab === TAB_PETS) && + } + { (currentTab === TAB_BADGES) && + } + { (currentTab === TAB_BOTS) && + } +
+
+ } + { isTrading && +
+ +
} +
+ + ); }; diff --git a/src/components/inventory/views/InventoryCategoryFilterView.tsx b/src/components/inventory/views/InventoryCategoryFilterView.tsx new file mode 100644 index 0000000..4c09e7b --- /dev/null +++ b/src/components/inventory/views/InventoryCategoryFilterView.tsx @@ -0,0 +1,114 @@ +import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'; +import { GroupItem, LocalizeBadgeName, LocalizeText } from '../../../api'; +import { NitroInput } from '../../../layout'; + +const FILTER_EVERYTHING = 'inventory.filter.option.everything'; +const FILTER_FLOOR = 'inventory.furni.tab.floor'; +const FILTER_WALL = 'inventory.furni.tab.wall'; + +const TAB_BADGES = 'inventory.badges'; +const TAB_FURNITURE = 'inventory.furni'; + +interface InventoryCategoryFilterViewProps +{ + currentTab: string; + groupItems: GroupItem[]; + badgeCodes: string[]; + setGroupItems: Dispatch>; + setBadgeCodes: Dispatch>; +} + +export const InventoryCategoryFilterView: FC = props => +{ + const { currentTab = null, groupItems = [], badgeCodes = [], setGroupItems = null, setBadgeCodes = null } = props; + const [ filterType, setFilterType ] = useState(FILTER_EVERYTHING); + const [ searchValue, setSearchValue ] = useState(''); + + useEffect(() => + { + if(currentTab !== TAB_BADGES) return; + + const comparison = searchValue.toLocaleLowerCase().replace(' ', ''); + + const filteredBadges = badgeCodes.filter(badge => badge.startsWith('ACH_')); + const numberMap: { [key: string]: number } = {}; + + filteredBadges.forEach(badge => + { + const name = badge.split(/[\d]+/)[0]; + const number = Number(badge.replace(name, '')); + + if(numberMap[name] === undefined || number > numberMap[name]) + { + numberMap[name] = number; + } + }); + + const deduped = Object.keys(numberMap) + .map(name => `${ name }${ numberMap[name] }`) + .concat(badgeCodes.filter(badge => !badge.startsWith('ACH_'))); + + const filtered = deduped.filter(badgeCode => + LocalizeBadgeName(badgeCode).toLocaleLowerCase().includes(comparison) + ); + + setBadgeCodes(filtered); + }, [ badgeCodes, currentTab, searchValue, setBadgeCodes ]); + + useEffect(() => + { + if(currentTab !== TAB_FURNITURE) return; + + const comparison = searchValue.toLocaleLowerCase(); + + if(filterType === FILTER_EVERYTHING) + { + setGroupItems(groupItems.filter(item => item.name.toLocaleLowerCase().includes(comparison))); + return; + } + + const filtered = groupItems.filter(item => + { + const isWall = filterType === FILTER_WALL ? item.isWallItem : false; + const isFloor = filterType === FILTER_FLOOR ? !item.isWallItem : false; + const matchesSearch = item.name.toLocaleLowerCase().includes(comparison); + + return comparison.length ? (matchesSearch && (isWall || isFloor)) : (isWall || isFloor); + }); + + setGroupItems(filtered); + }, [ groupItems, setGroupItems, searchValue, filterType, currentTab ]); + + useEffect(() => + { + setFilterType(FILTER_EVERYTHING); + setSearchValue(''); + }, [ currentTab ]); + + return ( +
+
+ setSearchValue(event.target.value) } /> + { (searchValue && searchValue.length > 0) && + setSearchValue('') } /> } +
+ { currentTab !== TAB_BADGES && + } +
+ ); +}; diff --git a/src/components/inventory/views/badge/InventoryBadgeView.tsx b/src/components/inventory/views/badge/InventoryBadgeView.tsx index 514ff8b..37b63c5 100644 --- a/src/components/inventory/views/badge/InventoryBadgeView.tsx +++ b/src/components/inventory/views/badge/InventoryBadgeView.tsx @@ -5,12 +5,15 @@ import { useInventoryBadges, useInventoryUnseenTracker } from '../../../../hooks import { InfiniteGrid, NitroButton } from '../../../../layout'; import { InventoryBadgeItemView } from './InventoryBadgeItemView'; -export const InventoryBadgeView: FC<{}> = props => +export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props => { + const { filteredBadgeCodes = null } = props; const [ isVisible, setIsVisible ] = useState(false); const { badgeCodes = [], activeBadgeCodes = [], selectedBadgeCode = null, isWearingBadge = null, canWearBadges = null, toggleBadge = null, getBadgeId = null, activate = null, deactivate = null } = useInventoryBadges(); const { isUnseen = null, removeUnseen = null } = useInventoryUnseenTracker(); + const displayCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes); + useEffect(() => { if(!selectedBadgeCode || !isUnseen(UnseenItemCategory.BADGE, getBadgeId(selectedBadgeCode))) return; @@ -41,7 +44,7 @@ export const InventoryBadgeView: FC<{}> = props => columnCount={ 5 } estimateSize={ 50 } itemRender={ item => } - items={ badgeCodes.filter(code => !isWearingBadge(code)) } /> + items={ displayCodes.filter(code => !isWearingBadge(code)) } />
@@ -54,7 +57,7 @@ export const InventoryBadgeView: FC<{}> = props =>
{ !!selectedBadgeCode &&
-
+
{ LocalizeBadgeName(selectedBadgeCode) }
diff --git a/src/components/inventory/views/furniture/InventoryFurnitureDeleteView.tsx b/src/components/inventory/views/furniture/InventoryFurnitureDeleteView.tsx new file mode 100644 index 0000000..d66ba03 --- /dev/null +++ b/src/components/inventory/views/furniture/InventoryFurnitureDeleteView.tsx @@ -0,0 +1,111 @@ +import { DeleteItemMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useState } from 'react'; +import { FaCaretLeft, FaCaretRight } from 'react-icons/fa'; +import { FurnitureItem, LocalizeText, ProductTypeEnum, SendMessageComposer } from '../../../../api'; +import { LayoutFurniImageView, NitroCardHeaderView, NitroCardView } from '../../../../common'; +import { DeleteItemConfirmEvent } from '../../../../events'; +import { useNotification, useUiEvent } from '../../../../hooks'; +import { NitroButton, NitroInput } from '../../../../layout'; + +export const InventoryFurnitureDeleteView: FC<{}> = props => +{ + const [ item, setItem ] = useState(null); + const [ amount, setAmount ] = useState(1); + const [ maxAmount, setMaxAmount ] = useState(1); + const { showConfirm = null } = useNotification(); + + const onClose = () => + { + setItem(null); + setAmount(1); + setMaxAmount(1); + }; + + const updateAmount = (value: string) => + { + let newValue = parseInt(value); + + if(isNaN(newValue)) newValue = 1; + + newValue = Math.max(newValue, 1); + newValue = Math.min(newValue, maxAmount); + + setAmount(newValue); + }; + + const deleteItem = () => + { + if(!item) return; + + const furniTitle = LocalizeText(item.isWallItem ? 'wallItem.name.' + item.type : 'roomItem.name.' + item.type); + + showConfirm( + LocalizeText('inventory.delete.confirm_delete.info', [ 'furniname', 'amount' ], [ furniTitle, amount.toString() ]), + () => + { + SendMessageComposer(new DeleteItemMessageComposer(item.id, amount)); + onClose(); + }, + () => onClose(), + null, + null, + LocalizeText('inventory.delete.confirm_delete.title') + ); + }; + + useUiEvent(DeleteItemConfirmEvent.DELETE_ITEM_CONFIRM, event => + { + setItem(event.item); + setMaxAmount(event.amount); + setAmount(1); + }); + + if(!item) return null; + + const furniTitle = LocalizeText(item.isWallItem ? 'wallItem.name.' + item.type : 'roomItem.name.' + item.type); + + return ( + + +
+
+
+ +
+
+ { furniTitle } +
+ updateAmount((amount - 1).toString()) } /> + updateAmount(event.target.value) } /> + updateAmount((amount + 1).toString()) } /> + updateAmount(maxAmount.toString()) }> + { LocalizeText('inventory.delete.max_amount.button') } + +
+ maxAmount } + onClick={ deleteItem }> + { LocalizeText('inventory.delete.confirm_delete.button') } + +
+
+
+
+ ); +}; diff --git a/src/components/inventory/views/furniture/InventoryFurnitureView.tsx b/src/components/inventory/views/furniture/InventoryFurnitureView.tsx index eb66106..a2fdd83 100644 --- a/src/components/inventory/views/furniture/InventoryFurnitureView.tsx +++ b/src/components/inventory/views/furniture/InventoryFurnitureView.tsx @@ -1,14 +1,14 @@ import { InfiniteGrid } from '@layout/InfiniteGrid'; import { GetRoomEngine, GetSessionDataManager, IRoomSession, RoomObjectVariable, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; +import { FaTrashAlt } from 'react-icons/fa'; import { DispatchUiEvent, FurniCategory, GroupItem, LocalizeText, UnseenItemCategory, attemptItemPlacement } from '../../../../api'; import { LayoutLimitedEditionCompactPlateView, LayoutRarityLevelView, LayoutRoomPreviewerView } from '../../../../common'; -import { CatalogPostMarketplaceOfferEvent } from '../../../../events'; +import { CatalogPostMarketplaceOfferEvent, DeleteItemConfirmEvent } from '../../../../events'; import { useInventoryFurni, useInventoryUnseenTracker } from '../../../../hooks'; import { NitroButton } from '../../../../layout'; import { InventoryCategoryEmptyView } from '../InventoryCategoryEmptyView'; import { InventoryFurnitureItemView } from './InventoryFurnitureItemView'; -import { InventoryFurnitureSearchView } from './InventoryFurnitureSearchView'; const attemptPlaceMarketplaceOffer = (groupItem: GroupItem) => { @@ -21,14 +21,23 @@ const attemptPlaceMarketplaceOffer = (groupItem: GroupItem) => DispatchUiEvent(new CatalogPostMarketplaceOfferEvent(item)); }; +const attemptDeleteItem = (groupItem: GroupItem) => +{ + const item = groupItem.getLastItem(); + + if(!item) return; + + DispatchUiEvent(new DeleteItemConfirmEvent(item, groupItem.getTotalCount())); +}; + export const InventoryFurnitureView: FC<{ roomSession: IRoomSession; roomPreviewer: RoomPreviewer; + filteredGroupItems: GroupItem[]; }> = props => { - const { roomSession = null, roomPreviewer = null } = props; + const { roomSession = null, roomPreviewer = null, filteredGroupItems = [] } = props; const [ isVisible, setIsVisible ] = useState(false); - const [ filteredGroupItems, setFilteredGroupItems ] = useState([]); const { groupItems = [], selectedItem = null, activate = null, deactivate = null } = useInventoryFurni(); const { resetItems = null } = useInventoryUnseenTracker(); @@ -112,7 +121,6 @@ export const InventoryFurnitureView: FC<{ return (
- columnCount={ 6 } itemRender={ item => } @@ -121,25 +129,33 @@ export const InventoryFurnitureView: FC<{
+ { selectedItem && + attemptDeleteItem(selectedItem) }> + + } { selectedItem && selectedItem.stuffData.isUnique && - } + } { (selectedItem && selectedItem.stuffData.rarityLevel > -1) && - } + }
{ selectedItem && -
- { selectedItem.name } -
- { !!roomSession && - attemptItemPlacement(selectedItem) }> - { LocalizeText('inventory.furni.placetoroom') } - } - { (selectedItem && selectedItem.isSellable) && - attemptPlaceMarketplaceOffer(selectedItem) }> - { LocalizeText('inventory.marketplace.sell') } - } -
-
} +
+ { selectedItem.name } + { selectedItem.description && + { selectedItem.description } } +
+ { !!roomSession && + attemptItemPlacement(selectedItem) }> + { LocalizeText('inventory.furni.placetoroom') } + } + { selectedItem.isSellable && + attemptPlaceMarketplaceOffer(selectedItem) }> + { LocalizeText('inventory.marketplace.sell') } + } +
+
}
); diff --git a/src/components/inventory/views/pet/InventoryPetView.tsx b/src/components/inventory/views/pet/InventoryPetView.tsx index 081890f..043fdd9 100644 --- a/src/components/inventory/views/pet/InventoryPetView.tsx +++ b/src/components/inventory/views/pet/InventoryPetView.tsx @@ -1,8 +1,9 @@ -import { GetRoomEngine, IRoomSession, RoomObjectVariable, RoomPreviewer } from '@nitrots/nitro-renderer'; +import { DeletePetMessageComposer, GetRoomEngine, IRoomSession, RoomObjectVariable, RoomPreviewer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { IPetItem, LocalizeText, UnseenItemCategory, attemptPetPlacement } from '../../../../api'; +import { FaTrashAlt } from 'react-icons/fa'; +import { IPetItem, LocalizeText, SendMessageComposer, UnseenItemCategory, attemptPetPlacement } from '../../../../api'; import { LayoutRoomPreviewerView } from '../../../../common'; -import { useInventoryPets, useInventoryUnseenTracker } from '../../../../hooks'; +import { useInventoryPets, useInventoryUnseenTracker, useNotification } from '../../../../hooks'; import { InfiniteGrid, NitroButton } from '../../../../layout'; import { InventoryCategoryEmptyView } from '../InventoryCategoryEmptyView'; import { InventoryPetItemView } from './InventoryPetItemView'; @@ -16,6 +17,21 @@ export const InventoryPetView: FC<{ const [ isVisible, setIsVisible ] = useState(false); const { petItems = null, selectedPet = null, activate = null, deactivate = null } = useInventoryPets(); const { isUnseen = null, removeUnseen = null } = useInventoryUnseenTracker(); + const { showConfirm = null } = useNotification(); + + const attemptDeletePet = () => + { + if(!selectedPet?.petData) return; + + showConfirm( + LocalizeText('inventory.delete.confirm_delete.info', [ 'furniname', 'amount' ], [ selectedPet.petData.name, '1' ]), + () => SendMessageComposer(new DeletePetMessageComposer(selectedPet.petData.id)), + null, + null, + null, + LocalizeText('inventory.delete.confirm_delete.title') + ); + }; useEffect(() => { @@ -74,6 +90,12 @@ export const InventoryPetView: FC<{
+ { selectedPet && + + + }
{ selectedPet && selectedPet.petData &&
diff --git a/src/css/inventory/Inventory.css b/src/css/inventory/Inventory.css new file mode 100644 index 0000000..b01e469 --- /dev/null +++ b/src/css/inventory/Inventory.css @@ -0,0 +1,29 @@ +:root { + --inventory-width: 528px; + --inventory-height: 420px; +} + +.nitro-inventory { + width: var(--inventory-width); + height: var(--inventory-height); + min-width: var(--inventory-width); + min-height: var(--inventory-height); +} + +.empty-image { + background: url("@/assets/images/inventory/empty.png"); + background-repeat: no-repeat; + width: 129px; + height: 181px; +} + +.trade-button { + min-height: 0; + font-size: 8px; + padding: 1px 2px; + z-index: 5; +} + +.quantity-input { + width: 49px; +} diff --git a/src/events/inventory/DeleteItemConfirmEvent.ts b/src/events/inventory/DeleteItemConfirmEvent.ts new file mode 100644 index 0000000..08612e1 --- /dev/null +++ b/src/events/inventory/DeleteItemConfirmEvent.ts @@ -0,0 +1,14 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; +import { FurnitureItem } from '../../api'; + +export class DeleteItemConfirmEvent extends NitroEvent +{ + public static DELETE_ITEM_CONFIRM: string = 'DICE_DELETE_ITEM_CONFIRM'; + + constructor( + public readonly item: FurnitureItem, + public readonly amount: number) + { + super(DeleteItemConfirmEvent.DELETE_ITEM_CONFIRM); + } +} diff --git a/src/events/inventory/index.ts b/src/events/inventory/index.ts index 58503ea..c8b627d 100644 --- a/src/events/inventory/index.ts +++ b/src/events/inventory/index.ts @@ -1 +1,2 @@ +export * from './DeleteItemConfirmEvent'; export * from './InventoryFurniAddedEvent'; diff --git a/src/index.tsx b/src/index.tsx index fb3de22..550d472 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,6 +19,8 @@ import './css/hotelview/HotelView.css'; import './css/icons/icons.css'; +import './css/inventory/Inventory.css'; + import './css/layout/LayoutTrophy.css'; import './css/loading/loading.css';