Merge branch '@feature/jukebox' into 'dev'

@feature/jukebox

See merge request nitro/nitro-react!101
This commit is contained in:
Bill 2022-11-16 04:21:45 +00:00
commit 6cef3564cf
31 changed files with 696 additions and 38 deletions

View File

@ -0,0 +1,37 @@
const DISK_COLOR_RED_MIN: number = 130;
const DISK_COLOR_RED_RANGE: number = 100;
const DISK_COLOR_GREEN_MIN: number = 130;
const DISK_COLOR_GREEN_RANGE: number = 100;
const DISK_COLOR_BLUE_MIN: number = 130;
const DISK_COLOR_BLUE_RANGE: number = 100;
export const GetDiskColor = (name: string) =>
{
let r: number = 0;
let g: number = 0;
let b: number = 0;
let index: number = 0;
while (index < name.length)
{
switch ((index % 3))
{
case 0:
r = (r + ( name.charCodeAt(index) * 37) );
break;
case 1:
g = (g + ( name.charCodeAt(index) * 37) );
break;
case 2:
b = (b + ( name.charCodeAt(index) * 37) );
break;
}
index++;
}
r = ((r % DISK_COLOR_RED_RANGE) + DISK_COLOR_RED_MIN);
g = ((g % DISK_COLOR_GREEN_RANGE) + DISK_COLOR_GREEN_MIN);
b = ((b % DISK_COLOR_BLUE_RANGE) + DISK_COLOR_BLUE_MIN);
return `rgb(${ r },${ g },${ b })`;
}

View File

@ -10,6 +10,7 @@ export * from './ChatMessageTypeEnum';
export * from './DimmerFurnitureWidgetPresetItem'; export * from './DimmerFurnitureWidgetPresetItem';
export * from './DoChatsOverlap'; export * from './DoChatsOverlap';
export * from './FurnitureDimmerUtilities'; export * from './FurnitureDimmerUtilities';
export * from './GetDiskColor';
export * from './IAvatarInfo'; export * from './IAvatarInfo';
export * from './ICraftingIngredient'; export * from './ICraftingIngredient';
export * from './ICraftingRecipe'; export * from './ICraftingRecipe';

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

View File

@ -200,6 +200,18 @@
height: 18px; height: 18px;
} }
&.disk-icon {
background-image: url('../images/infostand/disk-icon.png');
width: 14px;
height: 14px;
}
&.disk-creator {
background-image: url('../images/infostand/disk-creator.png');
width: 14px;
height: 14px;
}
&.trade-locked-icon { &.trade-locked-icon {
background-image: url("../images/inventory/trading/locked-icon.png"); background-image: url("../images/inventory/trading/locked-icon.png");
width: 29px; width: 29px;
@ -595,6 +607,7 @@
width: 17px; width: 17px;
height: 17px; height: 17px;
} }
&.icon-user { &.icon-user {
background-image: url("../images/icons/user.png"); background-image: url("../images/icons/user.png");
width: 18px; width: 18px;

View File

@ -52,7 +52,7 @@ export const LayoutPetImageView: FC<LayoutPetImageViewProps> = props =>
let petTypeId = typeId; let petTypeId = typeId;
let petPaletteId = paletteId; let petPaletteId = paletteId;
let petColor1 = petColor; let petColor1 = petColor;
let petCustomParts = customParts; let petCustomParts: IPetCustomPart[] = customParts;
let petHeadOnly = headOnly; let petHeadOnly = headOnly;
if(figure && figure.length) if(figure && figure.length)

View File

@ -0,0 +1,113 @@
import { GetOfficialSongIdMessageComposer, MusicPriorities, OfficialSongIdMessageEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { GetConfiguration, GetNitroInstance, LocalizeText, ProductTypeEnum, SendMessageComposer } from '../../../../../api';
import { Button, Column, Flex, Grid, LayoutImage, Text } from '../../../../../common';
import { useCatalog, useMessageEvent } from '../../../../../hooks';
import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView';
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
import { CatalogLimitedItemWidgetView } from '../widgets/CatalogLimitedItemWidgetView';
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
import { CatalogSpinnerWidgetView } from '../widgets/CatalogSpinnerWidgetView';
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView';
import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayoutSoundMachineView: FC<CatalogLayoutProps> = props =>
{
const { page = null } = props;
const [ songId, setSongId ] = useState(-1);
const [ officialSongId, setOfficialSongId ] = useState('');
const { currentOffer = null, currentPage = null } = useCatalog();
const previewSong = (previewSongId: number) => GetNitroInstance().soundManager.musicController?.playSong(previewSongId, MusicPriorities.PRIORITY_PURCHASE_PREVIEW, 15, 0, 0, 0);
useMessageEvent<OfficialSongIdMessageEvent>(OfficialSongIdMessageEvent, event =>
{
const parser = event.getParser();
if(parser.officialSongId !== officialSongId) return;
setSongId(parser.songId);
});
useEffect(() =>
{
if(!currentOffer) return;
const product = currentOffer.product;
if(!product) return;
if(product.extraParam.length > 0)
{
const id = parseInt(product.extraParam);
if(id > 0)
{
setSongId(id);
}
else
{
setOfficialSongId(product.extraParam);
SendMessageComposer(new GetOfficialSongIdMessageComposer(product.extraParam));
}
}
else
{
setOfficialSongId('');
setSongId(-1);
}
return () => GetNitroInstance().soundManager.musicController?.stop(MusicPriorities.PRIORITY_PURCHASE_PREVIEW);
}, [ currentOffer ]);
useEffect(() =>
{
return () => GetNitroInstance().soundManager.musicController?.stop(MusicPriorities.PRIORITY_PURCHASE_PREVIEW);
}, []);
return (
<>
<Grid>
<Column size={ 7 } overflow="hidden">
{ GetConfiguration('catalog.headers') &&
<CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) }/> }
<CatalogItemGridWidgetView />
</Column>
<Column center={ !currentOffer } size={ 5 } overflow="hidden">
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) &&
<LayoutImage imageUrl={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<Flex center overflow="hidden" style={ { height: 140 } }>
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) &&
<>
<CatalogViewProductWidgetView />
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 end-1" />
</> }
{ (currentOffer.product.productType === ProductTypeEnum.BADGE) && <CatalogAddOnBadgeWidgetView className="scale-2" /> }
</Flex>
<Column grow gap={ 1 }>
<CatalogLimitedItemWidgetView fullWidth />
<Text grow truncate>{ currentOffer.localizationName }</Text>
{ songId > -1 && <Button onClick={ () => previewSong(songId) }>{ LocalizeText('play_preview_button') }</Button>
}
<Flex justifyContent="between">
<Column gap={ 1 }>
<CatalogSpinnerWidgetView />
</Column>
<CatalogTotalPriceWidget justifyContent="end" alignItems="end" />
</Flex>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
</>
);
}

View File

@ -12,6 +12,7 @@ import { CatalogLayoutPets3View } from './CatalogLayoutPets3View';
import { CatalogLayoutRoomAdsView } from './CatalogLayoutRoomAdsView'; import { CatalogLayoutRoomAdsView } from './CatalogLayoutRoomAdsView';
import { CatalogLayoutRoomBundleView } from './CatalogLayoutRoomBundleView'; import { CatalogLayoutRoomBundleView } from './CatalogLayoutRoomBundleView';
import { CatalogLayoutSingleBundleView } from './CatalogLayoutSingleBundleView'; import { CatalogLayoutSingleBundleView } from './CatalogLayoutSingleBundleView';
import { CatalogLayoutSoundMachineView } from './CatalogLayoutSoundMachineView';
import { CatalogLayoutSpacesView } from './CatalogLayoutSpacesView'; import { CatalogLayoutSpacesView } from './CatalogLayoutSpacesView';
import { CatalogLayoutTrophiesView } from './CatalogLayoutTrophiesView'; import { CatalogLayoutTrophiesView } from './CatalogLayoutTrophiesView';
import { CatalogLayoutVipBuyView } from './CatalogLayoutVipBuyView'; import { CatalogLayoutVipBuyView } from './CatalogLayoutVipBuyView';
@ -69,6 +70,8 @@ export const GetCatalogLayout = (page: ICatalogPage, hideNavigation: () => void)
return <CatalogLayoutRoomAdsView { ...layoutProps } />; return <CatalogLayoutRoomAdsView { ...layoutProps } />;
case 'default_3x3_color_grouping': case 'default_3x3_color_grouping':
return <CatalogLayoutColorGroupingView { ...layoutProps } />; return <CatalogLayoutColorGroupingView { ...layoutProps } />;
case 'soundmachine':
return <CatalogLayoutSoundMachineView { ...layoutProps } />;
case 'bots': case 'bots':
case 'default_3x3': case 'default_3x3':
default: default:

View File

@ -1,9 +1,9 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { CrackableDataType, GroupInformationComposer, GroupInformationEvent, RoomControllerLevel, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomWidgetEnumItemExtradataParameter, RoomWidgetFurniInfoUsagePolicyEnum, SetObjectDataMessageComposer, StringDataType } from '@nitrots/nitro-renderer'; import { CrackableDataType, GroupInformationComposer, GroupInformationEvent, NowPlayingEvent, RoomControllerLevel, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomWidgetEnumItemExtradataParameter, RoomWidgetFurniInfoUsagePolicyEnum, SetObjectDataMessageComposer, SongInfoReceivedEvent, StringDataType } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react'; import { FC, useCallback, useEffect, useState } from 'react';
import { AvatarInfoFurni, CreateLinkEvent, GetGroupInformation, GetRoomEngine, LocalizeText, SendMessageComposer } from '../../../../../api'; import { AvatarInfoFurni, CreateLinkEvent, GetGroupInformation, GetNitroInstance, GetRoomEngine, LocalizeText, SendMessageComposer } from '../../../../../api';
import { Button, Column, Flex, LayoutBadgeImageView, LayoutLimitedEditionCompactPlateView, LayoutRarityLevelView, Text, UserProfileIconView } from '../../../../../common'; import { Base, Button, Column, Flex, LayoutBadgeImageView, LayoutLimitedEditionCompactPlateView, LayoutRarityLevelView, Text, UserProfileIconView } from '../../../../../common';
import { useMessageEvent, useRoom } from '../../../../../hooks'; import { useMessageEvent, useRoom, useSoundEvent } from '../../../../../hooks';
interface InfoStandWidgetFurniViewProps interface InfoStandWidgetFurniViewProps
{ {
@ -34,6 +34,28 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
const [ godMode, setGodMode ] = useState(false); const [ godMode, setGodMode ] = useState(false);
const [ canSeeFurniId, setCanSeeFurniId ] = useState(false); const [ canSeeFurniId, setCanSeeFurniId ] = useState(false);
const [ groupName, setGroupName ] = useState<string>(null); const [ groupName, setGroupName ] = useState<string>(null);
const [ isJukeBox, setIsJukeBox ] = useState<boolean>(false);
const [ isSongDisk, setIsSongDisk ] = useState<boolean>(false);
const [ songId, setSongId ] = useState<number>(-1);
const [ songName, setSongName ] = useState<string>('');
const [ songCreator, setSongCreator ] = useState<string>('');
useSoundEvent<NowPlayingEvent>(NowPlayingEvent.NPE_SONG_CHANGED, event =>
{
setSongId(event.id);
}, (isJukeBox || isSongDisk));
useSoundEvent<NowPlayingEvent>(SongInfoReceivedEvent.SIR_TRAX_SONG_INFO_RECEIVED, event =>
{
if(event.id !== songId) return;
const songInfo = GetNitroInstance().soundManager.musicController.getSongInfo(event.id);
if(!songInfo) return;
setSongName(songInfo.name);
setSongCreator(songInfo.creator);
}, (isJukeBox || isSongDisk));
useEffect(() => useEffect(() =>
{ {
@ -50,6 +72,9 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
let crackableTarget = 0; let crackableTarget = 0;
let godMode = false; let godMode = false;
let canSeeFurniId = false; let canSeeFurniId = false;
let furniIsJukebox = false;
let furniIsSongDisk = false;
let furniSongId = -1;
const isValidController = (avatarInfo.roomControllerLevel >= RoomControllerLevel.GUEST); const isValidController = (avatarInfo.roomControllerLevel >= RoomControllerLevel.GUEST);
@ -80,6 +105,25 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
crackableTarget = stuffData.target; crackableTarget = stuffData.target;
} }
else if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX)
{
const playlist = GetNitroInstance().soundManager.musicController.getRoomItemPlaylist();
if(playlist)
{
furniSongId = playlist.nowPlayingSongId;
}
furniIsJukebox = true;
}
else if(avatarInfo.extraParam.indexOf(RoomWidgetEnumItemExtradataParameter.SONGDISK) === 0)
{
furniSongId = parseInt(avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.SONGDISK.length));
furniIsSongDisk = true;
}
if(godMode) if(godMode)
{ {
const extraParam = avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.BRANDING_OPTIONS.length); const extraParam = avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.BRANDING_OPTIONS.length);
@ -142,6 +186,9 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
setGodMode(godMode); setGodMode(godMode);
setCanSeeFurniId(canSeeFurniId); setCanSeeFurniId(canSeeFurniId);
setGroupName(null); setGroupName(null);
setIsJukeBox(furniIsJukebox);
setIsSongDisk(furniIsSongDisk);
setSongId(furniSongId);
if(avatarInfo.groupId) SendMessageComposer(new GroupInformationComposer(avatarInfo.groupId, false)); if(avatarInfo.groupId) SendMessageComposer(new GroupInformationComposer(avatarInfo.groupId, false));
}, [ roomSession, avatarInfo ]); }, [ roomSession, avatarInfo ]);
@ -157,6 +204,14 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
setGroupName(parser.title); setGroupName(parser.title);
}); });
useEffect(() =>
{
const songInfo = GetNitroInstance().soundManager.musicController.getSongInfo(songId);
setSongName(songInfo?.name ?? '');
setSongCreator(songInfo?.creator ?? '');
}, [ songId ]);
const onFurniSettingChange = useCallback((index: number, value: string) => const onFurniSettingChange = useCallback((index: number, value: string) =>
{ {
const clone = Array.from(furniValues); const clone = Array.from(furniValues);
@ -315,6 +370,28 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
</Text> </Text>
</Flex> } </Flex> }
</Column> </Column>
{ (isJukeBox || isSongDisk) &&
<Column gap={ 1 }>
<hr className="m-0" />
{ (songId === -1) &&
<Text variant="white" small wrap>
{ LocalizeText('infostand.jukebox.text.not.playing') }
</Text> }
{ !!songName.length &&
<Flex alignItems="center" gap={ 1 }>
<Base className="icon disk-icon" />
<Text variant="white" small wrap>
{ songName }
</Text>
</Flex> }
{ !!songCreator.length &&
<Flex alignItems="center" gap={ 1 }>
<Base className="icon disk-creator" />
<Text variant="white" small wrap>
{ songCreator }
</Text>
</Flex> }
</Column> }
<Column gap={ 1 }> <Column gap={ 1 }>
{ isCrackable && { isCrackable &&
<> <>

View File

@ -33,8 +33,8 @@
} }
.nitro-widget-crafting { .nitro-widget-crafting {
width: $nitro-widget-crafting-width; width: $nitro-widget-crafting-width;
height: $nitro-widget-crafting-height; height: $nitro-widget-crafting-height;
} }
.nitro-widget-exchange-credit { .nitro-widget-exchange-credit {
@ -49,7 +49,6 @@
} }
.nitro-external-image-widget { .nitro-external-image-widget {
.picture-preview { .picture-preview {
width: 320px; width: 320px;
height: 320px; height: 320px;
@ -62,7 +61,8 @@
color: black; color: black;
} }
.picture-preview-buttons-previous, .picture-preview-buttons-next { .picture-preview-buttons-previous,
.picture-preview-buttons-next {
color: #222; color: #222;
background-color: white; background-color: white;
padding: 10px; padding: 10px;
@ -221,14 +221,14 @@
width: 31px; width: 31px;
height: 39px; height: 39px;
background-position: -380px -43px; background-position: -380px -43px;
background-image: url('../../../../assets/images/room-widgets/engraving-lock-widget/engraving-lock-spritesheet.png'); background-image: url("../../../../assets/images/room-widgets/engraving-lock-widget/engraving-lock-spritesheet.png");
} }
.engraving-lock-stage-2 { .engraving-lock-stage-2 {
width: 36px; width: 36px;
height: 43px; height: 43px;
background-position: -375px 0px; background-position: -375px 0px;
background-image: url('../../../../assets/images/room-widgets/engraving-lock-widget/engraving-lock-spritesheet.png'); background-image: url("../../../../assets/images/room-widgets/engraving-lock-widget/engraving-lock-spritesheet.png");
} }
} }
@ -236,7 +236,7 @@
width: 375px; width: 375px;
height: 210px; height: 210px;
background-position: 0px 0px; background-position: 0px 0px;
background-image: url('../../../../assets/images/room-widgets/engraving-lock-widget/engraving-lock-spritesheet.png'); background-image: url("../../../../assets/images/room-widgets/engraving-lock-widget/engraving-lock-spritesheet.png");
color: #622e54; color: #622e54;
font-weight: bold; font-weight: bold;
@ -251,7 +251,7 @@
&.engraving-lock-4 { &.engraving-lock-4 {
background-position: 0px -420px; background-position: 0px -420px;
color: #f1dcc8; color: #f1dcc8;
text-shadow: 0px 2px rgba(0, 0, 0, .4); text-shadow: 0px 2px rgba(0, 0, 0, 0.4);
.engraving-lock-avatar { .engraving-lock-avatar {
margin-bottom: 10px; margin-bottom: 10px;
@ -338,6 +338,7 @@
.playlist-controls { .playlist-controls {
width: 100%; width: 100%;
.icon { .icon {
margin-right: 10px; margin-right: 10px;
margin-bottom: 10px; margin-bottom: 10px;
@ -350,3 +351,104 @@
} }
} }
} }
.nitro-playlist-editor-widget {
width: 625px;
height: 440px;
img.my-music {
position: absolute;
top: -4px;
left: -4px;
z-index: 0;
}
img.playlist-img {
position: absolute;
top: -4px;
left: 0;
z-index: 0;
}
img.get-more,
img.add-songs {
position: absolute;
bottom: 0;
left: 0;
z-index: 0;
}
.playlist-bottom {
z-index: 3;
}
.move-disk {
width: 22px;
height: 18px;
background-image: url("../../../../assets/images/room-widgets/playlist-editor/move.png");
}
.disk-2,
.disk-image {
background-blend-mode: multiply;
}
.disk-2 {
width: 38px;
height: 38px;
background-image: url("../../../../assets/images/room-widgets/playlist-editor/disk_2.png");
background-position: center;
background-repeat: no-repeat;
&.playing-song {
background-image: url("../../../../assets/images/room-widgets/playlist-editor/playing.png");
}
&.selected-song {
background-image: url("../../../../assets/images/room-widgets/playlist-editor/move.png");
transform: scaleX(-1);
}
&:not(.playing-song):not(.selected-song) {
-webkit-mask-image: url("../../../../assets/images/room-widgets/playlist-editor/disk_2.png");
}
}
.pause-song {
width: 18px;
height: 20px;
background-image: url("../../../../assets/images/room-widgets/playlist-editor/pause.png");
}
.pause-btn {
width: 16px;
height: 16px;
background-image: url("../../../../assets/images/room-widgets/playlist-editor/pause-btn.png");
}
.music-note {
width: 38px;
height: 38px;
background-image: url("../../../../assets/images/room-widgets/playlist-editor/playing.png");
}
.preview-song {
width: 16px;
height: 16px;
background-image: url("../../../../assets/images/room-widgets/playlist-editor/preview.png");
}
.layout-grid-item {
min-height: 95px;
min-width: 95px;
position: relative;
.disk-image {
background: url("../../../../assets/images/room-widgets/playlist-editor/disk_image.png");
-webkit-mask-image: url("../../../../assets/images/room-widgets/playlist-editor/disk_image.png");
height: 76px;
width: 76px;
}
}
}

View File

@ -18,6 +18,7 @@ import { FurnitureStackHeightView } from './FurnitureStackHeightView';
import { FurnitureStickieView } from './FurnitureStickieView'; import { FurnitureStickieView } from './FurnitureStickieView';
import { FurnitureTrophyView } from './FurnitureTrophyView'; import { FurnitureTrophyView } from './FurnitureTrophyView';
import { FurnitureYoutubeDisplayView } from './FurnitureYoutubeDisplayView'; import { FurnitureYoutubeDisplayView } from './FurnitureYoutubeDisplayView';
import { FurniturePlaylistEditorWidgetView } from './playlist-editor/FurniturePlaylistEditorWidgetView';
export const FurnitureWidgetsView: FC<{}> = props => export const FurnitureWidgetsView: FC<{}> = props =>
{ {
@ -34,6 +35,7 @@ export const FurnitureWidgetsView: FC<{}> = props =>
<FurnitureHighScoreView /> <FurnitureHighScoreView />
<FurnitureInternalLinkView /> <FurnitureInternalLinkView />
<FurnitureMannequinView /> <FurnitureMannequinView />
<FurniturePlaylistEditorWidgetView />
<FurnitureRoomLinkView /> <FurnitureRoomLinkView />
<FurnitureSpamWallPostItView /> <FurnitureSpamWallPostItView />
<FurnitureStackHeightView /> <FurnitureStackHeightView />

View File

@ -0,0 +1,89 @@
import { IAdvancedMap, MusicPriorities } from '@nitrots/nitro-renderer';
import { FC, MouseEvent, useCallback, useEffect, useState } from 'react';
import { GetConfiguration, GetDiskColor, GetNitroInstance, LocalizeText } from '../../../../../api';
import { AutoGrid, Base, Button, Flex, LayoutGridItem, Text } from '../../../../../common';
export interface DiskInventoryViewProps
{
diskInventory: IAdvancedMap<number, number>;
addToPlaylist: (diskId: number, slotNumber: number) => void;
}
export const DiskInventoryView: FC<DiskInventoryViewProps> = props =>
{
const { diskInventory = null, addToPlaylist = null } = props;
const [ selectedItem, setSelectedItem ] = useState<number>(-1);
const [ previewSongId, setPreviewSongId ] = useState<number>(-1);
const previewSong = useCallback((event: MouseEvent, songId: number) =>
{
event.stopPropagation();
setPreviewSongId(prevValue => (prevValue === songId) ? -1 : songId);
}, []);
const addSong = useCallback((event: MouseEvent, diskId: number) =>
{
event.stopPropagation();
addToPlaylist(diskId, GetNitroInstance().soundManager.musicController?.getRoomItemPlaylist()?.length)
}, [ addToPlaylist ]);
useEffect(() =>
{
if(previewSongId === -1) return;
GetNitroInstance().soundManager.musicController?.playSong(previewSongId, MusicPriorities.PRIORITY_SONG_PLAY, 0, 0, 0, 0);
return () =>
{
GetNitroInstance().soundManager.musicController?.stop(MusicPriorities.PRIORITY_SONG_PLAY);
}
}, [ previewSongId ]);
useEffect(() =>
{
return () => setPreviewSongId(-1);
}, []);
return (<>
<div className="bg-success py-3 container-fluid justify-content-center d-flex rounded">
<img src={ GetConfiguration('image.library.url') + 'playlist/title_mymusic.gif' } className="my-music" />
<h2 className="ms-4">{ LocalizeText('playlist.editor.my.music') }</h2>
</div>
<div className="h-100 overflow-y-scroll mt-4 py-2">
<AutoGrid columnCount={ 3 } columnMinWidth={ 95 } gap={ 1 }>
{ diskInventory && diskInventory.getKeys().map( (key, index) =>
{
const diskId = diskInventory.getKey(index);
const songId = diskInventory.getWithIndex(index);
const songData = GetNitroInstance().soundManager.musicController?.getSongInfo(songId);
return (
<LayoutGridItem key={ index } itemActive={ (selectedItem === index) } onClick={ () => setSelectedItem(prev => prev === index ? -1 : index) } classNames={ [ 'text-black' ] }>
<div className="disk-image flex-shrink-0 mb-n2" style={ { backgroundColor: GetDiskColor(songData?.songData) } }>
</div>
<Text truncate fullWidth className="text-center">{ songData?.name }</Text>
{ (selectedItem === index) &&
<Flex position="absolute" className="bottom-0 mb-1 bg-secondary p-1 rounded" alignItems="center" justifyContent="center" gap={ 2 }>
<Button onClick={ event => previewSong(event, songId) } variant="light">
<Base className={ (previewSongId === songId) ? 'pause-btn' : 'preview-song' }/>
</Button>
<Button onClick={ event => addSong(event, diskId) } variant="light">
<Base className="move-disk"/>
</Button>
</Flex>
}
</LayoutGridItem>)
}) }
</AutoGrid>
</div>
<div className="playlist-bottom text-black p-1">
<h5>{ LocalizeText('playlist.editor.text.get.more.music') }</h5>
<div>{ LocalizeText('playlist.editor.text.you.have.no.songdisks.available') }</div>
<div>{ LocalizeText('playlist.editor.text.you.can.buy.some.from.the.catalogue') }</div>
<button className="btn btn-primary btn-sm">{ LocalizeText('playlist.editor.button.open.catalogue') }</button>
</div>
<img src={ GetConfiguration('image.library.url') + 'playlist/background_get_more_music.gif' } className="get-more" />
</>);
}

View File

@ -0,0 +1,29 @@
import { FC } from 'react';
import { LocalizeText } from '../../../../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../common';
import { useFurniturePlaylistEditorWidget } from '../../../../../hooks';
import { DiskInventoryView } from './DiskInventoryView';
import { SongPlaylistView } from './SongPlaylistView';
export const FurniturePlaylistEditorWidgetView: FC<{}> = props =>
{
const { objectId = -1, currentPlayingIndex = -1, playlist = null, diskInventory = null, onClose = null, togglePlayPause = null, removeFromPlaylist = null, addToPlaylist = null } = useFurniturePlaylistEditorWidget();
if(objectId === -1) return null;
return (
<NitroCardView className="nitro-playlist-editor-widget" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('playlist.editor.title') } onCloseClick={ onClose } />
<NitroCardContentView>
<div className="d-flex flex-row gap-1 h-100">
<div className="w-50 position-relative overflow-hidden h-100 rounded d-flex flex-column">
<DiskInventoryView addToPlaylist={ addToPlaylist } diskInventory={ diskInventory } />
</div>
<div className="w-50 position-relative overflow-hidden h-100 rounded d-flex flex-column">
<SongPlaylistView furniId={ objectId } removeFromPlaylist={ removeFromPlaylist } playlist={ playlist } togglePlayPause={ togglePlayPause } currentPlayingIndex={ currentPlayingIndex }/>
</div>
</div>
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -0,0 +1,79 @@
import { ISongInfo } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { GetConfiguration, GetDiskColor, LocalizeText } from '../../../../../api';
import { Base, Button, Flex, Text } from '../../../../../common';
export interface SongPlaylistViewProps
{
furniId: number;
playlist: ISongInfo[];
currentPlayingIndex: number;
removeFromPlaylist(slotNumber: number): void;
togglePlayPause(furniId: number, position: number): void;
}
export const SongPlaylistView: FC<SongPlaylistViewProps> = props =>
{
const { furniId = -1, playlist = null, currentPlayingIndex = -1, removeFromPlaylist = null, togglePlayPause = null } = props;
const [ selectedItem, setSelectedItem ] = useState<number>(-1);
const action = (index: number) =>
{
if(selectedItem === index) removeFromPlaylist(index);
}
const playPause = (furniId: number, selectedItem: number) =>
{
togglePlayPause(furniId, selectedItem !== -1 ? selectedItem : 0 )
}
return (<>
<div className="bg-primary py-3 container-fluid justify-content-center d-flex rounded">
<img src={ GetConfiguration('image.library.url') + 'playlist/title_playlist.gif' } className="playlist-img" />
<h2 className="ms-4">{ LocalizeText('playlist.editor.playlist') }</h2>
</div>
<div className="h-100 overflow-y-scroll py-2">
<Flex column gap={ 2 }>
{ playlist && playlist.map( (songInfo, index) =>
{
return <Flex gap={ 1 } key={ index } className={ 'text-black cursor-pointer ' + (selectedItem === index ? 'border border-muted border-2 rounded' : 'border-2') } alignItems="center" onClick={ () => setSelectedItem(prev => prev === index ? -1 : index) }>
<Base onClick={ () => action(index) } className={ 'disk-2 ' + (selectedItem === index ? 'selected-song' : '') } style={ { backgroundColor: (selectedItem === index ? '' : GetDiskColor(songInfo.songData)) } }/>
{ songInfo.name }
</Flex>
}) }
</Flex>
</div>
{ (!playlist || playlist.length === 0 ) &&
<><div className="playlist-bottom text-black p-1 ms-5">
<h5>{ LocalizeText('playlist.editor.add.songs.to.your.playlist') }</h5>
<div>{ LocalizeText('playlist.editor.text.click.song.to.choose.click.again.to.move') }</div>
</div>
<img src={ GetConfiguration('image.library.url') + 'playlist/background_add_songs.gif' } className="add-songs" /></>
}
{ (playlist && playlist.length > 0) &&
<>
{ (currentPlayingIndex === -1) &&
<Button variant="success" size="lg" onClick={ () => playPause(furniId, selectedItem) }>
{ LocalizeText('playlist.editor.button.play.now') }
</Button>
}
{ (currentPlayingIndex !== -1) &&
<Flex gap={ 1 }>
<Button variant="danger" onClick={ () => playPause(furniId, selectedItem) }>
<Base className="pause-song"/>
</Button>
<Flex column>
<Text bold display="block">{ LocalizeText('playlist.editor.text.now.playing.in.your.room') }</Text>
<Text>
{ playlist[currentPlayingIndex].name + ' - ' + playlist[currentPlayingIndex].creator }
</Text>
</Flex>
</Flex>
}
</>
}
</>);
}

View File

@ -5,3 +5,4 @@ export * from './useMainEvent';
export * from './useRoomEngineEvent'; export * from './useRoomEngineEvent';
export * from './useRoomSessionManagerEvent'; export * from './useRoomSessionManagerEvent';
export * from './useSessionDataManagerEvent'; export * from './useSessionDataManagerEvent';
export * from './useSoundEvent';

View File

@ -0,0 +1,5 @@
import { NitroEvent } from '@nitrots/nitro-renderer';
import { GetNitroInstance } from '../../../api';
import { useEventDispatcher } from '../useEventDispatcher';
export const useSoundEvent = <T extends NitroEvent>(type: string | string[], handler: (event: T) => void, enabled = true) => useEventDispatcher(type, GetNitroInstance().soundManager.events, handler, enabled);

View File

@ -382,7 +382,7 @@ const useNotificationState = () =>
useMessageEvent<RoomEnterEvent>(RoomEnterEvent, onRoomEnterEvent); useMessageEvent<RoomEnterEvent>(RoomEnterEvent, onRoomEnterEvent);
return { alerts, bubbleAlerts, confirms, simpleAlert, showNitroAlert, showTradeAlert, showConfirm, closeAlert, closeBubbleAlert, closeConfirm }; return { alerts, bubbleAlerts, confirms, simpleAlert, showNitroAlert, showTradeAlert, showConfirm, showSingleBubble, closeAlert, closeBubbleAlert, closeConfirm };
} }
export const useNotification = () => useBetween(useNotificationState); export const useNotification = () => useBetween(useNotificationState);

View File

@ -9,6 +9,7 @@ export * from './useFurnitureFriendFurniWidget';
export * from './useFurnitureHighScoreWidget'; export * from './useFurnitureHighScoreWidget';
export * from './useFurnitureInternalLinkWidget'; export * from './useFurnitureInternalLinkWidget';
export * from './useFurnitureMannequinWidget'; export * from './useFurnitureMannequinWidget';
export * from './useFurniturePlaylistEditorWidget';
export * from './useFurniturePresentWidget'; export * from './useFurniturePresentWidget';
export * from './useFurnitureRoomLinkWidget'; export * from './useFurnitureRoomLinkWidget';
export * from './useFurnitureSpamWallPostItWidget'; export * from './useFurnitureSpamWallPostItWidget';

View File

@ -35,10 +35,9 @@ const useFurnitureExternalImageWidgetState = () =>
if (object.type !== 'external_image_wallitem_poster_small') return null; if (object.type !== 'external_image_wallitem_poster_small') return null;
const data = object.model.getValue<string>(RoomObjectVariable.FURNITURE_DATA); const data = object.model.getValue<string>(RoomObjectVariable.FURNITURE_DATA);
const ownerId = object.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_ID); const jsonData: IPhotoData = JSON.parse(data);
const ownerName = object.model.getValue<string>(RoomObjectVariable.FURNITURE_OWNER_NAME);
datas.push({ s: JSON.parse(data).s, t: JSON.parse(data).t, u: JSON.parse(data).u, w: JSON.parse(data).w, oi: ownerId, o: ownerName }); datas.push(jsonData);
}); });
setObjectId(event.objectId); setObjectId(event.objectId);

View File

@ -0,0 +1,107 @@
import { AddJukeboxDiskComposer, AdvancedMap, FurnitureListAddOrUpdateEvent, FurnitureListEvent, FurnitureListRemovedEvent, FurnitureMultiStateComposer, IAdvancedMap, IMessageEvent, ISongInfo, NotifyPlayedSongEvent, NowPlayingEvent, PlayListStatusEvent, RemoveJukeboxDiskComposer, RoomControllerLevel, RoomEngineTriggerWidgetEvent, SongDiskInventoryReceivedEvent } from '@nitrots/nitro-renderer';
import { useCallback, useState } from 'react';
import { GetNitroInstance, GetRoomEngine, GetSessionDataManager, IsOwnerOfFurniture, LocalizeText, NotificationAlertType, NotificationBubbleType, SendMessageComposer } from '../../../../api';
import { useMessageEvent, useRoomEngineEvent, useSoundEvent } from '../../../events';
import { useNotification } from '../../../notification';
import { useFurniRemovedEvent } from '../../engine';
import { useRoom } from '../../useRoom';
const useFurniturePlaylistEditorWidgetState = () =>
{
const [ objectId, setObjectId ] = useState(-1);
const [ category, setCategory ] = useState(-1);
const [ currentPlayingIndex, setCurrentPlayingIndex ] = useState(-1);
const [ diskInventory, setDiskInventory ] = useState<IAdvancedMap<number, number>>(new AdvancedMap());
const [ playlist, setPlaylist ] = useState<ISongInfo[]>([]);
const { roomSession = null } = useRoom();
const { showSingleBubble = null, simpleAlert = null } = useNotification();
const onClose = () =>
{
setObjectId(-1);
setCategory(-1);
}
const addToPlaylist = useCallback((diskId: number, slotNumber: number) => SendMessageComposer(new AddJukeboxDiskComposer(diskId, slotNumber)), []);
const removeFromPlaylist = useCallback((slotNumber: number) => SendMessageComposer(new RemoveJukeboxDiskComposer(slotNumber)), []);
const togglePlayPause = useCallback((furniId: number, position: number) => SendMessageComposer(new FurnitureMultiStateComposer(furniId, position)), []);
useRoomEngineEvent<RoomEngineTriggerWidgetEvent>(RoomEngineTriggerWidgetEvent.REQUEST_PLAYLIST_EDITOR, event =>
{
const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category);
if(!roomObject) return;
if(IsOwnerOfFurniture(roomObject))
{
// show the editor
setObjectId(event.objectId);
setCategory(event.category);
GetNitroInstance().soundManager.musicController?.requestUserSongDisks();
GetNitroInstance().soundManager.musicController?.getRoomItemPlaylist()?.requestPlayList();
return;
}
if(roomSession.isRoomOwner || (roomSession.controllerLevel >= RoomControllerLevel.GUEST) || GetSessionDataManager().isModerator) SendMessageComposer(new FurnitureMultiStateComposer(event.objectId, -2));
});
useFurniRemovedEvent(((objectId !== -1) && (category !== -1)), event =>
{
if((event.id !== objectId) || (event.category !== category)) return;
onClose();
});
useSoundEvent<NowPlayingEvent>(NowPlayingEvent.NPE_SONG_CHANGED, event =>
{
console.log(event);
setCurrentPlayingIndex(event.position);
});
useSoundEvent<NotifyPlayedSongEvent>(NotifyPlayedSongEvent.NOTIFY_PLAYED_SONG, event =>
{
showSingleBubble(LocalizeText('soundmachine.notification.playing', [ 'songname', 'songauthor' ], [ event.name, event.creator ]), NotificationBubbleType.SOUNDMACHINE)
});
useSoundEvent<SongDiskInventoryReceivedEvent>(SongDiskInventoryReceivedEvent.SDIR_SONG_DISK_INVENTORY_RECEIVENT_EVENT, event =>
{
setDiskInventory(GetNitroInstance().soundManager.musicController?.songDiskInventory.clone());
});
useSoundEvent<PlayListStatusEvent>(PlayListStatusEvent.PLUE_PLAY_LIST_UPDATED, event =>
{
setPlaylist(GetNitroInstance().soundManager.musicController?.getRoomItemPlaylist()?.entries.concat())
});
useSoundEvent<PlayListStatusEvent>(PlayListStatusEvent.PLUE_PLAY_LIST_FULL, event =>
{
simpleAlert(LocalizeText('playlist.editor.alert.playlist.full'), NotificationAlertType.ALERT, '', '', LocalizeText('playlist.editor.alert.playlist.full.title'));
});
const onFurniListUpdated = useCallback((event : IMessageEvent) =>
{
if(event instanceof FurnitureListEvent)
{
if(event.getParser().fragmentNumber === 0)
{
GetNitroInstance().soundManager.musicController?.requestUserSongDisks();
}
}
else
{
GetNitroInstance().soundManager.musicController?.requestUserSongDisks();
}
}, []);
useMessageEvent(FurnitureListEvent, onFurniListUpdated);
useMessageEvent(FurnitureListRemovedEvent, onFurniListUpdated);
useMessageEvent(FurnitureListAddOrUpdateEvent, onFurniListUpdated);
return { objectId, diskInventory, playlist, currentPlayingIndex, onClose, addToPlaylist, removeFromPlaylist, togglePlayPause };
}
export const useFurniturePlaylistEditorWidget = useFurniturePlaylistEditorWidgetState;