mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
Merge pull request #3 from simoleo89/pr/badge-drag-drop
feat(badges): add drag & drop system for InfoStand and inventory
This commit is contained in:
@@ -11,8 +11,22 @@ export const InventoryBadgeItemView: FC<PropsWithChildren<{ badgeCode: string }>
|
||||
const { isUnseen = null } = useInventoryUnseenTracker();
|
||||
const unseen = isUnseen(UnseenItemCategory.BADGE, getBadgeId(badgeCode));
|
||||
|
||||
const onDragStart = (event: React.DragEvent<HTMLDivElement>) =>
|
||||
{
|
||||
event.dataTransfer.setData('badgeCode', badgeCode);
|
||||
event.dataTransfer.setData('source', 'inventory');
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
|
||||
return (
|
||||
<InfiniteGrid.Item itemActive={ (selectedBadgeCode === badgeCode) } itemUnseen={ unseen } onDoubleClick={ event => toggleBadge(selectedBadgeCode) } onMouseDown={ event => setSelectedBadgeCode(badgeCode) } { ...rest }>
|
||||
<InfiniteGrid.Item
|
||||
draggable
|
||||
itemActive={ (selectedBadgeCode === badgeCode) }
|
||||
itemUnseen={ unseen }
|
||||
onDoubleClick={ event => toggleBadge(selectedBadgeCode) }
|
||||
onDragStart={ onDragStart }
|
||||
onMouseDown={ event => setSelectedBadgeCode(badgeCode) }
|
||||
{ ...rest }>
|
||||
<LayoutBadgeImageView badgeCode={ badgeCode } />
|
||||
{ children }
|
||||
</InfiniteGrid.Item>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DeleteBadgeMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { FaTrashAlt } from 'react-icons/fa';
|
||||
import { LocalizeBadgeName, LocalizeText, SendMessageComposer, UnseenItemCategory } from '../../../../api';
|
||||
import { LayoutBadgeImageView } from '../../../../common';
|
||||
@@ -7,14 +7,74 @@ import { useInventoryBadges, useInventoryUnseenTracker, useNotification } from '
|
||||
import { InfiniteGrid, NitroButton } from '../../../../layout';
|
||||
import { InventoryBadgeItemView } from './InventoryBadgeItemView';
|
||||
|
||||
const ActiveBadgeSlot: FC<{
|
||||
slotIndex: number;
|
||||
badgeCode?: string;
|
||||
onDropBadge: (badgeCode: string, slotIndex: number, sourceSlot?: number) => void;
|
||||
onRemoveBadge: (badgeCode: string) => void;
|
||||
onDragStartFromSlot: (event: React.DragEvent, badgeCode: string, slotIndex: number) => void;
|
||||
onSelectBadge: (badgeCode: string) => void;
|
||||
isSelected: boolean;
|
||||
}> = ({ slotIndex, badgeCode, onDropBadge, onRemoveBadge, onDragStartFromSlot, onSelectBadge, isSelected }) =>
|
||||
{
|
||||
const [ isDragOver, setIsDragOver ] = useState(false);
|
||||
|
||||
const onDragOver = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
setIsDragOver(true);
|
||||
}, []);
|
||||
|
||||
const onDragLeave = useCallback(() => setIsDragOver(false), []);
|
||||
|
||||
const onDrop = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
setIsDragOver(false);
|
||||
|
||||
const droppedBadgeCode = event.dataTransfer.getData('badgeCode');
|
||||
const sourceSlotStr = event.dataTransfer.getData('activeSlot');
|
||||
const sourceSlot = sourceSlotStr ? parseInt(sourceSlotStr) : undefined;
|
||||
|
||||
if(droppedBadgeCode) onDropBadge(droppedBadgeCode, slotIndex, sourceSlot);
|
||||
}, [ slotIndex, onDropBadge ]);
|
||||
|
||||
const onDragStart = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
if(!badgeCode) return;
|
||||
onDragStartFromSlot(event, badgeCode, slotIndex);
|
||||
}, [ badgeCode, slotIndex, onDragStartFromSlot ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ `flex items-center justify-center rounded-md border-2 cursor-pointer aspect-square transition-colors
|
||||
${ isDragOver ? 'border-blue-400 bg-blue-400/20' : '' }
|
||||
${ isSelected && badgeCode ? 'border-card-grid-item-active bg-card-grid-item-active' : 'border-card-grid-item-border bg-card-grid-item' }
|
||||
${ !badgeCode ? 'border-dashed opacity-60' : '' }` }
|
||||
draggable={ !!badgeCode }
|
||||
onDragLeave={ onDragLeave }
|
||||
onDragOver={ onDragOver }
|
||||
onDragStart={ onDragStart }
|
||||
onDrop={ onDrop }
|
||||
onMouseDown={ () => badgeCode && onSelectBadge(badgeCode) }>
|
||||
{ badgeCode
|
||||
? <LayoutBadgeImageView badgeCode={ badgeCode } />
|
||||
: <span className="text-xs text-white/30">{ slotIndex + 1 }</span> }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 { badgeCodes = [], activeBadgeCodes = [], selectedBadgeCode = null, isWearingBadge = null, canWearBadges = null, toggleBadge = null, getBadgeId = null, setBadgeAtSlot = null, removeBadge = null, reorderBadges = null, setSelectedBadgeCode = null, activate = null, deactivate = null } = useInventoryBadges();
|
||||
const { isUnseen = null, removeUnseen = null } = useInventoryUnseenTracker();
|
||||
const { showConfirm = null } = useNotification();
|
||||
const [ isDragOverInventory, setIsDragOverInventory ] = useState(false);
|
||||
|
||||
const maxSlots = 5;
|
||||
const displayCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes);
|
||||
|
||||
const attemptDeleteBadge = () =>
|
||||
@@ -31,6 +91,58 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
|
||||
);
|
||||
};
|
||||
|
||||
const handleDropOnSlot = useCallback((badgeCode: string, slotIndex: number, sourceSlot?: number) =>
|
||||
{
|
||||
if(sourceSlot !== undefined)
|
||||
{
|
||||
// Reorder within active badges
|
||||
reorderBadges(sourceSlot, slotIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Drop from inventory to active slot
|
||||
setBadgeAtSlot(badgeCode, slotIndex);
|
||||
}
|
||||
}, [ setBadgeAtSlot, reorderBadges ]);
|
||||
|
||||
const handleDragStartFromSlot = useCallback((event: React.DragEvent, badgeCode: string, slotIndex: number) =>
|
||||
{
|
||||
event.dataTransfer.setData('badgeCode', badgeCode);
|
||||
event.dataTransfer.setData('activeSlot', slotIndex.toString());
|
||||
event.dataTransfer.setData('source', 'active');
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
}, []);
|
||||
|
||||
const handleRemoveBadge = useCallback((badgeCode: string) =>
|
||||
{
|
||||
removeBadge(badgeCode);
|
||||
}, [ removeBadge ]);
|
||||
|
||||
// Handle drop on inventory area (remove from active)
|
||||
const onInventoryDragOver = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
const source = event.dataTransfer.types.includes('activeslot') ? 'active' : '';
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
setIsDragOverInventory(true);
|
||||
}, []);
|
||||
|
||||
const onInventoryDragLeave = useCallback(() => setIsDragOverInventory(false), []);
|
||||
|
||||
const onInventoryDrop = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
setIsDragOverInventory(false);
|
||||
|
||||
const badgeCode = event.dataTransfer.getData('badgeCode');
|
||||
const source = event.dataTransfer.getData('source');
|
||||
|
||||
if(source === 'active' && badgeCode)
|
||||
{
|
||||
removeBadge(badgeCode);
|
||||
}
|
||||
}, [ removeBadge ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!selectedBadgeCode || !isUnseen(UnseenItemCategory.BADGE, getBadgeId(selectedBadgeCode))) return;
|
||||
@@ -56,7 +168,11 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
|
||||
|
||||
return (
|
||||
<div className="grid h-full grid-cols-12 gap-2">
|
||||
<div className="flex flex-col col-span-7 gap-1 overflow-hidden">
|
||||
<div
|
||||
className={ `flex flex-col col-span-7 gap-1 overflow-hidden rounded transition-colors ${ isDragOverInventory ? 'bg-blue-400/10' : '' }` }
|
||||
onDragLeave={ onInventoryDragLeave }
|
||||
onDragOver={ onInventoryDragOver }
|
||||
onDrop={ onInventoryDrop }>
|
||||
<InfiniteGrid<string>
|
||||
columnCount={ 5 }
|
||||
estimateSize={ 50 }
|
||||
@@ -66,11 +182,20 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
|
||||
<div className="flex flex-col justify-between col-span-5 overflow-auto">
|
||||
<div className="flex flex-col gap-2 overflow-hidden">
|
||||
<span className="text-sm truncate min-h-[1.25rem] leading-5">{ LocalizeText('inventory.badges.activebadges') }</span>
|
||||
<InfiniteGrid<string>
|
||||
columnCount={ 3 }
|
||||
estimateSize={ 50 }
|
||||
itemRender={ item => <InventoryBadgeItemView badgeCode={ item } /> }
|
||||
items={ activeBadgeCodes } />
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{ Array.from({ length: maxSlots }).map((_, index) => (
|
||||
<ActiveBadgeSlot
|
||||
key={ index }
|
||||
badgeCode={ activeBadgeCodes[index] }
|
||||
isSelected={ selectedBadgeCode === activeBadgeCodes[index] && !!activeBadgeCodes[index] }
|
||||
slotIndex={ index }
|
||||
onDropBadge={ handleDropOnSlot }
|
||||
onDragStartFromSlot={ handleDragStartFromSlot }
|
||||
onRemoveBadge={ handleRemoveBadge }
|
||||
onSelectBadge={ setSelectedBadgeCode }
|
||||
/>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
{ !!selectedBadgeCode &&
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { LayoutBadgeImageView } from '../../../../../common';
|
||||
import { useInventoryBadges } from '../../../../../hooks';
|
||||
|
||||
interface InfoStandBadgeSlotProps
|
||||
{
|
||||
slotIndex: number;
|
||||
badgeCode?: string;
|
||||
isOwnUser: boolean;
|
||||
}
|
||||
|
||||
const BadgeMiniPicker: FC<{
|
||||
onSelect: (badgeCode: string) => void;
|
||||
onClose: () => void;
|
||||
activeBadgeCodes: string[];
|
||||
}> = ({ onSelect, onClose, activeBadgeCodes }) =>
|
||||
{
|
||||
const { badgeCodes = [], requestBadges = null } = useInventoryBadges();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [ search, setSearch ] = useState('');
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(badgeCodes.length === 0) requestBadges();
|
||||
}, []);
|
||||
|
||||
const availableBadges = badgeCodes.filter(code => !activeBadgeCodes.includes(code));
|
||||
const filtered = search.length > 0
|
||||
? availableBadges.filter(code => code.toLowerCase().includes(search.toLowerCase()))
|
||||
: availableBadges;
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const handleClickOutside = (event: MouseEvent) =>
|
||||
{
|
||||
if(ref.current && !ref.current.contains(event.target as Node)) onClose();
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [ onClose ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ ref }
|
||||
className="absolute right-[calc(100%+8px)] top-0 z-50 bg-[rgba(28,28,32,0.97)] border border-white/20 rounded-md p-2 shadow-lg min-w-[160px]"
|
||||
onClick={ e => e.stopPropagation() }>
|
||||
<input
|
||||
autoFocus
|
||||
className="w-full text-xs text-white bg-white/10 border border-white/20 rounded px-2 py-1 mb-2 outline-none focus:border-white/40"
|
||||
placeholder="Cerca badge..."
|
||||
type="text"
|
||||
value={ search }
|
||||
onChange={ e => setSearch(e.target.value) }
|
||||
/>
|
||||
{ badgeCodes.length === 0
|
||||
? <span className="text-xs text-white/40 text-center py-2 block">Caricamento...</span>
|
||||
: (
|
||||
<div className="grid grid-cols-4 gap-1 max-h-[160px] overflow-y-auto">
|
||||
{ filtered.slice(0, 40).map(code => (
|
||||
<div
|
||||
key={ code }
|
||||
className="flex items-center justify-center w-[36px] h-[36px] cursor-pointer rounded border border-transparent hover:border-white/40 hover:bg-white/10 transition-all"
|
||||
onClick={ () => onSelect(code) }>
|
||||
<LayoutBadgeImageView badgeCode={ code } />
|
||||
</div>
|
||||
)) }
|
||||
{ filtered.length === 0 && (
|
||||
<span className="text-xs text-white/40 col-span-4 text-center py-2">Nessun badge</span>
|
||||
) }
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InfoStandBadgeSlotView: FC<InfoStandBadgeSlotProps> = ({ slotIndex, badgeCode: badgeCodeFromProps, isOwnUser }) =>
|
||||
{
|
||||
const { activeBadgeCodes = [], setBadgeAtSlot = null, swapBadges = null } = useInventoryBadges();
|
||||
const [ isDragOver, setIsDragOver ] = useState(false);
|
||||
const [ showPicker, setShowPicker ] = useState(false);
|
||||
|
||||
// For own user: use hook data if loaded, otherwise fall back to props (avatarInfo)
|
||||
// For other users: always use props
|
||||
const hookBadge = activeBadgeCodes.length > 0 ? (activeBadgeCodes[slotIndex] ?? null) : null;
|
||||
const badgeCode = isOwnUser ? (hookBadge ?? badgeCodeFromProps ?? null) : (badgeCodeFromProps ?? null);
|
||||
|
||||
const onDragStart = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
if(!badgeCode || !isOwnUser) return;
|
||||
event.dataTransfer.setData('badgeCode', badgeCode);
|
||||
event.dataTransfer.setData('infostandSlot', slotIndex.toString());
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
}, [ badgeCode, slotIndex, isOwnUser ]);
|
||||
|
||||
const onDragOver = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
if(!isOwnUser) return;
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
setIsDragOver(true);
|
||||
}, [ isOwnUser ]);
|
||||
|
||||
const onDragLeave = useCallback(() => setIsDragOver(false), []);
|
||||
|
||||
const onDrop = useCallback((event: React.DragEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
setIsDragOver(false);
|
||||
if(!isOwnUser) return;
|
||||
|
||||
const droppedBadgeCode = event.dataTransfer.getData('badgeCode');
|
||||
const sourceSlotStr = event.dataTransfer.getData('infostandSlot');
|
||||
|
||||
if(!droppedBadgeCode) return;
|
||||
|
||||
if(sourceSlotStr !== '')
|
||||
{
|
||||
// Dragged from another infostand slot -> always swap (works with empty slots too)
|
||||
const sourceSlot = parseInt(sourceSlotStr);
|
||||
|
||||
if(sourceSlot !== slotIndex) swapBadges(sourceSlot, slotIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dragged from inventory or external -> place at this slot
|
||||
setBadgeAtSlot(droppedBadgeCode, slotIndex);
|
||||
}
|
||||
}, [ isOwnUser, slotIndex, swapBadges, setBadgeAtSlot ]);
|
||||
|
||||
const handleSlotClick = useCallback(() =>
|
||||
{
|
||||
if(!isOwnUser || badgeCode) return;
|
||||
|
||||
setShowPicker(true);
|
||||
}, [ isOwnUser, badgeCode ]);
|
||||
|
||||
const handlePickerSelect = useCallback((code: string) =>
|
||||
{
|
||||
setBadgeAtSlot(code, slotIndex);
|
||||
setShowPicker(false);
|
||||
}, [ setBadgeAtSlot, slotIndex ]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
className={ `flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center transition-all duration-150
|
||||
${ isOwnUser && badgeCode ? 'cursor-grab active:cursor-grabbing' : '' }
|
||||
${ isOwnUser && !badgeCode ? 'cursor-pointer' : '' }
|
||||
${ isOwnUser ? 'hover:scale-110 hover:brightness-125 hover:drop-shadow-[0_0_6px_rgba(255,255,255,0.3)]' : '' }
|
||||
${ isDragOver ? 'scale-115 ring-2 ring-blue-400/60 rounded-sm bg-blue-400/15' : '' }
|
||||
${ isOwnUser && !badgeCode ? 'opacity-40 hover:opacity-70 border border-dashed border-white/20 rounded-sm' : '' }` }
|
||||
draggable={ isOwnUser && !!badgeCode }
|
||||
onDragLeave={ onDragLeave }
|
||||
onDragOver={ onDragOver }
|
||||
onDragStart={ onDragStart }
|
||||
onDrop={ onDrop }
|
||||
onClick={ handleSlotClick }>
|
||||
{ badgeCode
|
||||
? <LayoutBadgeImageView badgeCode={ badgeCode } showInfo={ true } />
|
||||
: isOwnUser && <FaPlus className="text-white/30 text-[10px]" /> }
|
||||
</div>
|
||||
{ showPicker && (
|
||||
<BadgeMiniPicker
|
||||
activeBadgeCodes={ activeBadgeCodes }
|
||||
onClose={ () => setShowPicker(false) }
|
||||
onSelect={ handlePickerSelect }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { FaPencilAlt, FaTimes } from 'react-icons/fa';
|
||||
import { AvatarInfoUser, CloneObject, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { Base, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserProfileIconView } from '../../../../../common';
|
||||
import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks';
|
||||
import { InfoStandBadgeSlotView } from './InfoStandBadgeSlotView';
|
||||
import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView';
|
||||
import { InfoStandWidgetUserTagsView } from './InfoStandWidgetUserTagsView';
|
||||
import { BackgroundsView } from '../../../../backgrounds/BackgroundsView';
|
||||
@@ -158,31 +159,43 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
|
||||
/>
|
||||
)}
|
||||
<Column grow alignItems="center" gap={0}>
|
||||
<div className="flex gap-1">
|
||||
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
|
||||
{avatarInfo.badges[0] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[0]} showInfo={true} />}
|
||||
</div>
|
||||
<Flex center className="relative w-[40px] h-[40px] bg-no-repeat bg-center" pointer={avatarInfo.groupId > 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}>
|
||||
{avatarInfo.groupId > 0 &&
|
||||
<LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />}
|
||||
</Flex>
|
||||
</div>
|
||||
<Flex center gap={1}>
|
||||
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
|
||||
{avatarInfo.badges[1] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[1]} showInfo={true} />}
|
||||
</div>
|
||||
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
|
||||
{avatarInfo.badges[2] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[2]} showInfo={true} />}
|
||||
</div>
|
||||
</Flex>
|
||||
<Flex center gap={1}>
|
||||
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
|
||||
{avatarInfo.badges[3] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[3]} showInfo={true} />}
|
||||
</div>
|
||||
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
|
||||
{avatarInfo.badges[4] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[4]} showInfo={true} />}
|
||||
</div>
|
||||
</Flex>
|
||||
{ GetConfigurationValue<boolean>('user.badges.group.slot.enabled', true)
|
||||
? (
|
||||
<>
|
||||
<div className="flex gap-1">
|
||||
<InfoStandBadgeSlotView slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<Flex center className="relative w-[40px] h-[40px] bg-no-repeat bg-center" pointer={avatarInfo.groupId > 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}>
|
||||
{avatarInfo.groupId > 0 &&
|
||||
<LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />}
|
||||
</Flex>
|
||||
</div>
|
||||
<Flex center gap={1}>
|
||||
<InfoStandBadgeSlotView slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<InfoStandBadgeSlotView slotIndex={2} badgeCode={avatarInfo.badges[2]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
</Flex>
|
||||
<Flex center gap={1}>
|
||||
<InfoStandBadgeSlotView slotIndex={3} badgeCode={avatarInfo.badges[3]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<InfoStandBadgeSlotView slotIndex={4} badgeCode={avatarInfo.badges[4]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Flex center gap={1}>
|
||||
<InfoStandBadgeSlotView slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<InfoStandBadgeSlotView slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
</Flex>
|
||||
<Flex center gap={1}>
|
||||
<InfoStandBadgeSlotView slotIndex={2} badgeCode={avatarInfo.badges[2]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<InfoStandBadgeSlotView slotIndex={3} badgeCode={avatarInfo.badges[3]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
</Flex>
|
||||
<Flex center gap={1}>
|
||||
<InfoStandBadgeSlotView slotIndex={4} badgeCode={avatarInfo.badges[4]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
<InfoStandBadgeSlotView slotIndex={5} badgeCode={avatarInfo.badges[5]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Column>
|
||||
</div>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BadgeReceivedEvent, BadgesEvent, RequestBadgesComposer, SetActivatedBadgesComposer } from '@nitrots/nitro-renderer';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useBetween } from 'use-between';
|
||||
import { GetConfigurationValue, SendMessageComposer, UnseenItemCategory } from '../../api';
|
||||
import { useMessageEvent } from '../events';
|
||||
@@ -17,9 +17,18 @@ const useInventoryBadgesState = () =>
|
||||
const { isUnseen = null, resetCategory = null } = useInventoryUnseenTracker();
|
||||
|
||||
const maxBadgeCount = GetConfigurationValue<number>('user.badges.max.slots', 5);
|
||||
const localChangeRef = useRef(false);
|
||||
const isWearingBadge = (badgeCode: string) => (activeBadgeCodes.indexOf(badgeCode) >= 0);
|
||||
const canWearBadges = () => (activeBadgeCodes.length < maxBadgeCount);
|
||||
|
||||
const sendActiveBadges = (badges: string[]) =>
|
||||
{
|
||||
localChangeRef.current = true;
|
||||
const composer = new SetActivatedBadgesComposer();
|
||||
for(let i = 0; i < maxBadgeCount; i++) composer.addActivatedBadge(badges[i] ?? '');
|
||||
SendMessageComposer(composer);
|
||||
};
|
||||
|
||||
const toggleBadge = (badgeCode: string) =>
|
||||
{
|
||||
setActiveBadgeCodes(prevValue =>
|
||||
@@ -30,7 +39,7 @@ const useInventoryBadgesState = () =>
|
||||
|
||||
if(index === -1)
|
||||
{
|
||||
if(!canWearBadges()) return prevValue;
|
||||
if(newValue.length >= maxBadgeCount) return prevValue;
|
||||
|
||||
newValue.push(badgeCode);
|
||||
}
|
||||
@@ -39,11 +48,7 @@ const useInventoryBadgesState = () =>
|
||||
newValue.splice(index, 1);
|
||||
}
|
||||
|
||||
const composer = new SetActivatedBadgesComposer();
|
||||
|
||||
for(let i = 0; i < maxBadgeCount; i++) composer.addActivatedBadge(newValue[i] ?? '');
|
||||
|
||||
SendMessageComposer(composer);
|
||||
sendActiveBadges(newValue);
|
||||
|
||||
return newValue;
|
||||
});
|
||||
@@ -77,7 +82,16 @@ const useInventoryBadgesState = () =>
|
||||
return newValue;
|
||||
});
|
||||
|
||||
setActiveBadgeCodes(parser.getActiveBadgeCodes());
|
||||
// Skip overwriting activeBadgeCodes if we recently made a local change
|
||||
if(localChangeRef.current)
|
||||
{
|
||||
localChangeRef.current = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
setActiveBadgeCodes(parser.getActiveBadgeCodes());
|
||||
}
|
||||
|
||||
setBadgeCodes(allBadgeCodes);
|
||||
});
|
||||
|
||||
@@ -141,7 +155,83 @@ const useInventoryBadgesState = () =>
|
||||
setNeedsUpdate(false);
|
||||
}, [ isVisible, needsUpdate ]);
|
||||
|
||||
return { badgeCodes, activeBadgeCodes, selectedBadgeCode, setSelectedBadgeCode, isWearingBadge, canWearBadges, toggleBadge, getBadgeId, activate, deactivate };
|
||||
const setBadgeAtSlot = (badgeCode: string, slotIndex: number) =>
|
||||
{
|
||||
setActiveBadgeCodes(prevValue =>
|
||||
{
|
||||
// Build a fixed-size array of maxBadgeCount slots
|
||||
const slots: (string | null)[] = Array.from({ length: maxBadgeCount }, (_, i) => prevValue[i] ?? null);
|
||||
|
||||
// Remove badge if already in another slot
|
||||
const existingIndex = slots.indexOf(badgeCode);
|
||||
if(existingIndex >= 0) slots[existingIndex] = null;
|
||||
|
||||
// Place badge at target slot
|
||||
slots[slotIndex] = badgeCode;
|
||||
|
||||
// Compact: remove nulls, keep order
|
||||
const result = slots.filter(Boolean) as string[];
|
||||
|
||||
sendActiveBadges(result);
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
const removeBadge = (badgeCode: string) =>
|
||||
{
|
||||
setActiveBadgeCodes(prevValue =>
|
||||
{
|
||||
const result = prevValue.filter(code => code !== badgeCode);
|
||||
|
||||
sendActiveBadges(result);
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
const reorderBadges = (fromIndex: number, toIndex: number) =>
|
||||
{
|
||||
setActiveBadgeCodes(prevValue =>
|
||||
{
|
||||
if(fromIndex === toIndex) return prevValue;
|
||||
if(fromIndex >= prevValue.length) return prevValue;
|
||||
|
||||
const newValue = [ ...prevValue ];
|
||||
const [ moved ] = newValue.splice(fromIndex, 1);
|
||||
newValue.splice(toIndex, 0, moved);
|
||||
|
||||
sendActiveBadges(newValue);
|
||||
return newValue;
|
||||
});
|
||||
};
|
||||
|
||||
const swapBadges = (fromIndex: number, toIndex: number) =>
|
||||
{
|
||||
setActiveBadgeCodes(prevValue =>
|
||||
{
|
||||
if(fromIndex === toIndex) return prevValue;
|
||||
|
||||
// Build fixed-size array so swap works even with empty slots
|
||||
const slots: (string | null)[] = Array.from({ length: maxBadgeCount }, (_, i) => prevValue[i] ?? null);
|
||||
|
||||
// Swap the two slots
|
||||
const temp = slots[fromIndex];
|
||||
slots[fromIndex] = slots[toIndex];
|
||||
slots[toIndex] = temp;
|
||||
|
||||
// Compact: remove nulls, keep order
|
||||
const result = slots.filter(Boolean) as string[];
|
||||
|
||||
sendActiveBadges(result);
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
const requestBadges = () =>
|
||||
{
|
||||
SendMessageComposer(new RequestBadgesComposer());
|
||||
};
|
||||
|
||||
return { badgeCodes, activeBadgeCodes, selectedBadgeCode, setSelectedBadgeCode, isWearingBadge, canWearBadges, toggleBadge, getBadgeId, setBadgeAtSlot, removeBadge, reorderBadges, swapBadges, requestBadges, activate, deactivate };
|
||||
};
|
||||
|
||||
export const useInventoryBadges = () => useBetween(useInventoryBadgesState);
|
||||
|
||||
Reference in New Issue
Block a user