diff --git a/src/api/room/widgets/ChooserSelectionVisualizer.ts b/src/api/room/widgets/ChooserSelectionVisualizer.ts new file mode 100644 index 0000000..b9bd248 --- /dev/null +++ b/src/api/room/widgets/ChooserSelectionVisualizer.ts @@ -0,0 +1,100 @@ +import { ChooserSelectionFilter, GetRoomEngine, IRoomObjectSpriteVisualization, RoomObjectCategory } from '@nitrots/nitro-renderer'; + +export class chooserSelectionVisualizer +{ + private static activeFilters: Map = new Map(); + private static animationFrameId: number | null = null; + + private static startAnimation(): void + { + if (this.animationFrameId !== null) return; + + const animate = (time: number) => { + const elapsed = time / 1000; // Convert to seconds + this.activeFilters.forEach(filter => { + filter.time = elapsed; // Update time uniform + }); + this.animationFrameId = requestAnimationFrame(animate); + }; + + this.animationFrameId = requestAnimationFrame(animate); + } + + private static stopAnimation(): void + { + if (this.animationFrameId !== null) { + cancelAnimationFrame(this.animationFrameId); + this.animationFrameId = null; + } + } + + public static show(id: number, category: number = RoomObjectCategory.FLOOR): void + { + const roomObject = GetRoomEngine().getRoomObject(GetRoomEngine().activeRoomId, id, category); + if (!roomObject) return; + + const visualization = roomObject.visualization as IRoomObjectSpriteVisualization; + if (!visualization || !visualization.sprites || !visualization.sprites.length) return; + + const filter = new ChooserSelectionFilter( + [0.700, 0.880, 0.950], + [0.290, 0.350, 0.390] + ); + const key = `${id}_${category}`; + this.activeFilters.set(key, filter); + + for (const sprite of visualization.sprites) + { + if (sprite.blendMode === 1) continue; + const existing = (sprite.filters || []).filter(f => !(f instanceof ChooserSelectionFilter)); + sprite.filters = [...existing, filter]; + } + + this.startAnimation(); + } + + public static hide(id: number, category: number = RoomObjectCategory.FLOOR): void + { + const roomObject = GetRoomEngine().getRoomObject(GetRoomEngine().activeRoomId, id, category); + if (!roomObject) return; + + const visualization = roomObject.visualization as IRoomObjectSpriteVisualization; + if (!visualization) return; + + const key = `${id}_${category}`; + this.activeFilters.delete(key); + + for (const sprite of visualization.sprites) + { + sprite.filters = (sprite.filters || []).filter(f => !(f instanceof ChooserSelectionFilter)); + } + + if (this.activeFilters.size === 0) { + this.stopAnimation(); + } + } + + public static clearAll(): void + { + const roomEngine = GetRoomEngine(); + + const roomObjects = [ + ...roomEngine.getRoomObjects(roomEngine.activeRoomId, RoomObjectCategory.FLOOR), + ...roomEngine.getRoomObjects(roomEngine.activeRoomId, RoomObjectCategory.WALL) + ]; + + for (const roomObject of roomObjects) + { + const visualization = roomObject.visualization as IRoomObjectSpriteVisualization; + if (!visualization) continue; + + for (const sprite of visualization.sprites) + { + sprite.filters = (sprite.filters || []).filter(f => !(f instanceof ChooserSelectionFilter)); + } + } + + this.activeFilters.clear(); + this.stopAnimation(); + } +} \ No newline at end of file diff --git a/src/api/room/widgets/RoomObjectItem.ts b/src/api/room/widgets/RoomObjectItem.ts index f4fb2d6..9c50b3d 100644 --- a/src/api/room/widgets/RoomObjectItem.ts +++ b/src/api/room/widgets/RoomObjectItem.ts @@ -3,12 +3,18 @@ export class RoomObjectItem private _id: number; private _category: number; private _name: string; + private _ownerId: number; + private _ownerName: string; + private _type?: string; - constructor(id: number, category: number, name: string) + constructor(id: number, category: number, name: string, ownerId: number = 0, ownerName: string = '#', type?: string) { this._id = id; this._category = category; this._name = name; + this._ownerId = ownerId; + this._ownerName = ownerName; + this._type = type; } public get id(): number @@ -25,4 +31,19 @@ export class RoomObjectItem { return this._name; } + + public get ownerId(): number + { + return this._ownerId; + } + + public get ownerName(): string + { + return this._ownerName ?? '#'; + } + + public get type(): string + { + return this._type ?? '-'; + } } diff --git a/src/api/room/widgets/index.ts b/src/api/room/widgets/index.ts index 6c50c83..5cef378 100644 --- a/src/api/room/widgets/index.ts +++ b/src/api/room/widgets/index.ts @@ -1,4 +1,5 @@ export * from './AvatarInfoFurni'; +export * from './ChooserSelectionVisualizer'; export * from './AvatarInfoName'; export * from './AvatarInfoPet'; export * from './AvatarInfoRentableBot'; diff --git a/src/components/room/widgets/choosers/ChooserWidgetView.tsx b/src/components/room/widgets/choosers/ChooserWidgetView.tsx index 9cd7162..e144f8d 100644 --- a/src/components/room/widgets/choosers/ChooserWidgetView.tsx +++ b/src/components/room/widgets/choosers/ChooserWidgetView.tsx @@ -1,88 +1,201 @@ -import { GetSessionDataManager, FurniturePickupAllComposer } from '@nitrots/nitro-renderer'; +import { FurniturePickupAllComposer, GetSessionDataManager } from '@nitrots/nitro-renderer'; import { FC, useEffect, useMemo, useState } from 'react'; -import { LocalizeText, RoomObjectItem, SendMessageComposer } from '../../../../api'; +import { LocalizeText, RoomObjectItem, SendMessageComposer, chooserSelectionVisualizer } from '../../../../api'; import { Button, Flex, InfiniteScroll, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; import { NitroInput, classNames } from '../../../../layout'; const LIMIT_FURNI_PICKALL = 100; -interface ChooserWidgetViewProps { +interface ChooserWidgetViewProps +{ title: string; items: RoomObjectItem[]; selectItem: (item: RoomObjectItem) => void; onClose: () => void; pickallFurni?: boolean; + type?: 'furni' | 'users'; } -export const ChooserWidgetView: FC = props => { - const { title = null, items = [], selectItem = null, onClose = null, pickallFurni = false } = props; - const [ selectedItem, setSelectedItem ] = useState(null); +export const ChooserWidgetView: FC = props => +{ + const { title = null, items = [], selectItem = null, onClose = null, pickallFurni = false, type = 'furni' } = props; + const [ selectedItems, setSelectedItems ] = useState([]); const [ searchValue, setSearchValue ] = useState(''); const [ checkAll, setCheckAll ] = useState(false); const [ checkedIds, setCheckedIds ] = useState([]); const canSeeId = GetSessionDataManager().isModerator; - const checkedId = (id?: number) => { - if (id) { - if (isChecked(id)) + const ownerNames = useMemo(() => + { + const names = Array.from(new Set(items.map(item => item.ownerName || 'Unknown'))); + return names.sort(); + }, [ items ]); + + const [ selectedFilter, setSelectedFilter ] = useState(() => + { + if(pickallFurni) return 'all'; + return ownerNames.length > 0 ? ownerNames[0] : ''; + }); + + useEffect(() => + { + if(!pickallFurni && ownerNames.length > 0 && !selectedFilter) + setSelectedFilter(ownerNames[0]); + }, [ pickallFurni, ownerNames, selectedFilter ]); + + const checkedId = (id?: number) => + { + if(id) + { + if(isChecked(id)) setCheckedIds(checkedIds.filter(x => x !== id)); - else if (checkedIds.length < LIMIT_FURNI_PICKALL) + else if(checkedIds.length < LIMIT_FURNI_PICKALL) setCheckedIds([ ...checkedIds, id ]); - } else { + } + else + { setCheckAll(value => !value); - if (!checkAll) { - const itemIds = filteredItems.map(x => x.id).slice(0, LIMIT_FURNI_PICKALL); - setCheckedIds(itemIds); - } else { + + if(!checkAll) + { + const allItems = filteredItems.slice(0, LIMIT_FURNI_PICKALL); + setCheckedIds(allItems.map(x => x.id)); + setSelectedItems(allItems); + } + else + { setCheckedIds([]); + setSelectedItems([]); + chooserSelectionVisualizer.clearAll(); } } } const isChecked = (id: number) => checkedIds.includes(id); - const onClickPickAll = () => { + const onClickPickAll = () => + { SendMessageComposer(new FurniturePickupAllComposer(...checkedIds)); setCheckedIds([]); setCheckAll(false); + chooserSelectionVisualizer.clearAll(); + setSelectedItems([]); } - const filteredItems = useMemo(() => { + const filteredItems = useMemo(() => + { const value = searchValue.toLocaleLowerCase(); - const itemsFilter = items.filter(item => item.name?.toLocaleLowerCase().includes(value)); - return itemsFilter.sort((a, b) => a.name.localeCompare(b.name)); - }, [ items, searchValue ]); - useEffect(() => { - if (!selectedItem) return; - selectItem(selectedItem); - }, [ selectedItem, selectItem ]); + return items + .filter(item => + { + const matchesSearch = item.name?.toLocaleLowerCase().includes(value); + const matchesFilter = !pickallFurni + ? (selectedFilter ? item.ownerName === selectedFilter : true) + : (selectedFilter === 'all' || item.ownerName === selectedFilter); + return matchesSearch && matchesFilter; + }) + .sort((a, b) => a.name.localeCompare(b.name)); + }, [ items, searchValue, selectedFilter, pickallFurni ]); + + useEffect(() => + { + if(selectedItems.length === 0) return; + + selectItem(selectedItems[selectedItems.length - 1]); + + chooserSelectionVisualizer.clearAll(); + selectedItems.forEach(item => + { + if(item.id && item.category) + chooserSelectionVisualizer.show(item.id, item.category); + }); + }, [ selectedItems, selectItem ]); + + const toggleItemSelection = (item: RoomObjectItem) => + { + setSelectedItems(prev => + { + if(prev.some(selected => selected.id === item.id)) + { + chooserSelectionVisualizer.hide(item.id, item.category); + return prev.filter(selected => selected.id !== item.id); + } + else + { + return [ ...prev, item ]; + } + }); + }; + + const handleClose = () => + { + chooserSelectionVisualizer.clearAll(); + setSelectedItems([]); + onClose(); + }; return ( - - - - setSearchValue(event.target.value) } /> + + + + + setSearchValue(event.target.value) } + /> + { pickallFurni && ( + + )} + { pickallFurni && ( - checkedId() } /> + checkedId() } + /> { LocalizeText('widget.chooser.checkall') } )} - ( - setSelectedItem(row) }> + ( + item.id === row.id) && 'bg-muted') } + pointer + onClick={ () => { toggleItemSelection(row); if(pickallFurni) checkedId(row.id); } } + > { pickallFurni && ( - checkedId(row.id) } - onClick={ e => e.stopPropagation() } + onClick={ e => { e.stopPropagation(); toggleItemSelection(row); } } /> )} - { row.name } { canSeeId && (' - ' + row.id) } + + { row.name }{ canSeeId && (' - ' + row.id) } + { type === 'furni' && row.ownerName && row.ownerName !== '-' && ` (Owner: ${row.ownerName})` } + - )} rows={ filteredItems } /> + )} /> { pickallFurni && (