diff --git a/package.json b/package.json index 3277a368..6a5eeca1 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "react-slider": "^1.3.1", "react-transition-group": "^4.4.2", "react-virtualized": "^9.22.3", + "react-youtube": "^7.13.1", "typescript": "^4.3.5", "web-vitals": "^1.1.2" }, diff --git a/src/api/nitro/room/widgets/events/RoomWidgetUpdateYoutubeDisplayEvent.ts b/src/api/nitro/room/widgets/events/RoomWidgetUpdateYoutubeDisplayEvent.ts new file mode 100644 index 00000000..0a4773e1 --- /dev/null +++ b/src/api/nitro/room/widgets/events/RoomWidgetUpdateYoutubeDisplayEvent.ts @@ -0,0 +1,27 @@ +import { RoomWidgetUpdateEvent } from './RoomWidgetUpdateEvent'; + +export class RoomWidgetUpdateYoutubeDisplayEvent extends RoomWidgetUpdateEvent +{ + public static UPDATE_YOUTUBE_DISPLAY: string = 'RWUEIE_UPDATE_YOUTUBE_DISPLAY'; + + private _objectId: number; + private _hasControl: boolean; + + constructor(objectId: number, hasControl = false) + { + super(RoomWidgetUpdateYoutubeDisplayEvent.UPDATE_YOUTUBE_DISPLAY); + + this._objectId = objectId; + this._hasControl = hasControl; + } + + public get objectId(): number + { + return this._objectId; + } + + public get hasControl(): boolean + { + return this._hasControl; + } +} diff --git a/src/api/nitro/room/widgets/handlers/FurnitureYoutubeDisplayWidgetHandler.ts b/src/api/nitro/room/widgets/handlers/FurnitureYoutubeDisplayWidgetHandler.ts new file mode 100644 index 00000000..a8c0a15b --- /dev/null +++ b/src/api/nitro/room/widgets/handlers/FurnitureYoutubeDisplayWidgetHandler.ts @@ -0,0 +1,74 @@ +import { SecurityLevel } from '@nitrots/nitro-renderer'; +import { NitroEvent } from '@nitrots/nitro-renderer/src/core/events/NitroEvent'; +import { GetYoutubeDisplayStatusMessageComposer } from '@nitrots/nitro-renderer/src/nitro/communication/messages/outgoing/room/furniture/youtube'; +import { RoomEngineTriggerWidgetEvent } from '@nitrots/nitro-renderer/src/nitro/room/events/RoomEngineTriggerWidgetEvent'; +import { RoomWidgetEnum } from '@nitrots/nitro-renderer/src/nitro/ui/widget/enums/RoomWidgetEnum'; +import { RoomWidgetMessage, RoomWidgetUpdateEvent } from '..'; +import { GetSessionDataManager, IsOwnerOfFurniture } from '../../..'; +import { SendMessageHook } from '../../../../../hooks'; +import { GetRoomEngine } from '../../GetRoomEngine'; +import { RoomWidgetUpdateYoutubeDisplayEvent } from '../events/RoomWidgetUpdateYoutubeDisplayEvent'; +import { RoomWidgetHandler } from './RoomWidgetHandler'; + +export class FurnitureYoutubeDisplayWidgetHandler extends RoomWidgetHandler +{ + public static readonly CONTROL_COMMAND_PREVIOUS_VIDEO = 0; + public static readonly CONTROL_COMMAND_NEXT_VIDEO = 1; + public static readonly CONTROL_COMMAND_PAUSE_VIDEO = 2; + public static readonly CONTROL_COMMAND_CONTINUE_VIDEO = 3; + + private _lastFurniId: number = -1; + + public processEvent(event: NitroEvent): void + { + switch(event.type) + { + case RoomEngineTriggerWidgetEvent.OPEN_WIDGET: { + const widgetEvent = (event as RoomEngineTriggerWidgetEvent); + + const roomObject = GetRoomEngine().getRoomObject(widgetEvent.roomId, widgetEvent.objectId, widgetEvent.category); + + if(!roomObject) return; + + this._lastFurniId = widgetEvent.objectId; + + const hasControl = GetSessionDataManager().hasSecurity(SecurityLevel.EMPLOYEE) || IsOwnerOfFurniture(roomObject); + this.container.eventDispatcher.dispatchEvent(new RoomWidgetUpdateYoutubeDisplayEvent(roomObject.id, hasControl)); + SendMessageHook(new GetYoutubeDisplayStatusMessageComposer(this._lastFurniId)); + return; + } + case RoomEngineTriggerWidgetEvent.CLOSE_WIDGET: { + const widgetEvent = (event as RoomEngineTriggerWidgetEvent); + + if(widgetEvent.objectId !== this._lastFurniId) return; + + this.container.eventDispatcher.dispatchEvent(new RoomWidgetUpdateYoutubeDisplayEvent(-1)); + return; + } + } + } + + public processWidgetMessage(message: RoomWidgetMessage): RoomWidgetUpdateEvent + { + switch(message.type) + { + } + + return null; + } + + public get type(): string + { + return RoomWidgetEnum.YOUTUBE; + } + + public get eventTypes(): string[] + { + return []; + } + + public get messageTypes(): string[] + { + return []; + } +} diff --git a/src/assets/images/room-widgets/youtube-widget/next.png b/src/assets/images/room-widgets/youtube-widget/next.png new file mode 100644 index 00000000..a02e164b Binary files /dev/null and b/src/assets/images/room-widgets/youtube-widget/next.png differ diff --git a/src/assets/images/room-widgets/youtube-widget/prev.png b/src/assets/images/room-widgets/youtube-widget/prev.png new file mode 100644 index 00000000..d48b658e Binary files /dev/null and b/src/assets/images/room-widgets/youtube-widget/prev.png differ diff --git a/src/assets/styles/icons.scss b/src/assets/styles/icons.scss index 78679240..87fd77ce 100644 --- a/src/assets/styles/icons.scss +++ b/src/assets/styles/icons.scss @@ -693,6 +693,17 @@ height: 16px; } + &.icon-youtube-next { + background: url("../images/room-widgets/youtube-widget/next.png"); + width: 21px; + height: 16px; + } + + &.icon-youtube-prev { + background: url("../images/room-widgets/youtube-widget/prev.png"); + width: 21px; + height: 16px; + } &.icon-friendlist-warning { background: url("../images/friendlist/icons/icon_warning.png"); width: 23px; diff --git a/src/views/room/RoomView.tsx b/src/views/room/RoomView.tsx index 2ad37017..b885b48c 100644 --- a/src/views/room/RoomView.tsx +++ b/src/views/room/RoomView.tsx @@ -1,6 +1,7 @@ import { EventDispatcher, NitroRectangle, RoomGeometry, RoomVariableEnum, Vector3d } from '@nitrots/nitro-renderer'; import { FC, useEffect, useRef, useState } from 'react'; import { DispatchMouseEvent, DispatchTouchEvent, DoorbellWidgetHandler, FurniChooserWidgetHandler, FurnitureContextMenuWidgetHandler, FurnitureCreditWidgetHandler, FurnitureCustomStackHeightWidgetHandler, FurnitureDimmerWidgetHandler, FurnitureExternalImageWidgetHandler, FurnitureMannequinWidgetHandler, FurniturePresentWidgetHandler, GetNitroInstance, GetRoomEngine, InitializeRoomInstanceRenderingCanvas, IRoomWidgetHandlerManager, RoomWidgetAvatarInfoHandler, RoomWidgetChatHandler, RoomWidgetChatInputHandler, RoomWidgetHandlerManager, RoomWidgetInfostandHandler, RoomWidgetRoomToolsHandler, RoomWidgetUpdateRoomViewEvent, UserChooserWidgetHandler } from '../../api'; +import { FurnitureYoutubeDisplayWidgetHandler } from '../../api/nitro/room/widgets/handlers/FurnitureYoutubeDisplayWidgetHandler'; import { RoomContextProvider } from './context/RoomContext'; import { RoomColorView } from './RoomColorView'; import { RoomViewProps } from './RoomView.types'; @@ -44,6 +45,7 @@ export const RoomView: FC = props => widgetHandlerManager.registerHandler(new FurnitureExternalImageWidgetHandler()); widgetHandlerManager.registerHandler(new FurniturePresentWidgetHandler()); widgetHandlerManager.registerHandler(new FurnitureDimmerWidgetHandler()); + widgetHandlerManager.registerHandler(new FurnitureYoutubeDisplayWidgetHandler()); widgetHandlerManager.registerHandler(new FurnitureMannequinWidgetHandler()); setWidgetHandler(widgetHandlerManager); diff --git a/src/views/room/widgets/furniture/FurnitureWidgets.scss b/src/views/room/widgets/furniture/FurnitureWidgets.scss index 563b6733..55ebd35f 100644 --- a/src/views/room/widgets/furniture/FurnitureWidgets.scss +++ b/src/views/room/widgets/furniture/FurnitureWidgets.scss @@ -13,3 +13,4 @@ @import './stickie/FurnitureStickieView'; @import './high-score/FurnitureHighScoreView'; @import './gift-opening/FurnitureGiftOpeningView'; +@import './youtube-tv/FurnitureYoutubeDisplayView'; diff --git a/src/views/room/widgets/furniture/FurnitureWidgetsView.tsx b/src/views/room/widgets/furniture/FurnitureWidgetsView.tsx index 16663637..553965d6 100644 --- a/src/views/room/widgets/furniture/FurnitureWidgetsView.tsx +++ b/src/views/room/widgets/furniture/FurnitureWidgetsView.tsx @@ -13,6 +13,7 @@ import { FurnitureManipulationMenuView } from './manipulation-menu/FurnitureMani import { FurnitureMannequinView } from './mannequin/FurnitureMannequinView'; import { FurnitureStickieView } from './stickie/FurnitureStickieView'; import { FurnitureTrophyView } from './trophy/FurnitureTrophyView'; +import { FurnitureYoutubeDisplayView } from './youtube-tv/FurnitureYoutubeDisplayView'; export const FurnitureWidgetsView: FC<{}> = props => { @@ -32,6 +33,7 @@ export const FurnitureWidgetsView: FC<{}> = props => + ); } diff --git a/src/views/room/widgets/furniture/youtube-tv/FurnitureYoutubeDisplayView.scss b/src/views/room/widgets/furniture/youtube-tv/FurnitureYoutubeDisplayView.scss new file mode 100644 index 00000000..c782b781 --- /dev/null +++ b/src/views/room/widgets/furniture/youtube-tv/FurnitureYoutubeDisplayView.scss @@ -0,0 +1,54 @@ +.youtube-tv-widget { + width: 600px; + height: 380px; + + .youtube-video-container { + //min-height: 366px; + + .empty-video { + background-color: black; + color: white; + width: 100%; + height: 100%; + text-align: center; + } + + .youtubeContainer { + position: relative; + width: 100%; + height: 100%; + //height: 0; + //padding-bottom: 56.25%; + overflow: hidden; + margin-bottom: 50px; + } + + .youtubeContainer iframe { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + } + } + + .playlist-container { + overflow-y: hidden; + margin-right: -10px; + color: black; + height: 100%; + + .playlist-controls { + width: 100%; + .icon { + margin-right: 10px; + margin-bottom: 10px; + } + } + + .playlist-grid { + height: 100%; + width: 100%; + } + } +} diff --git a/src/views/room/widgets/furniture/youtube-tv/FurnitureYoutubeDisplayView.tsx b/src/views/room/widgets/furniture/youtube-tv/FurnitureYoutubeDisplayView.tsx new file mode 100644 index 00000000..3121d944 --- /dev/null +++ b/src/views/room/widgets/furniture/youtube-tv/FurnitureYoutubeDisplayView.tsx @@ -0,0 +1,247 @@ +import { ControlYoutubeDisplayPlaybackMessageComposer, SetYoutubeDisplayPlaylistMessageComposer, YoutubeControlVideoMessageEvent, YoutubeDisplayPlaylist, YoutubeDisplayPlaylistsEvent, YoutubeDisplayVideoMessageEvent } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useMemo, useState } from 'react'; +import YouTube, { Options } from 'react-youtube'; +import { LocalizeText } from '../../../../../api'; +import { RoomWidgetUpdateYoutubeDisplayEvent } from '../../../../../api/nitro/room/widgets/events/RoomWidgetUpdateYoutubeDisplayEvent'; +import { FurnitureYoutubeDisplayWidgetHandler } from '../../../../../api/nitro/room/widgets/handlers/FurnitureYoutubeDisplayWidgetHandler'; +import { BatchUpdates, CreateEventDispatcherHook, CreateMessageHook, SendMessageHook } from '../../../../../hooks'; +import { NitroCardContentView, NitroCardGridItemView, NitroCardGridView, NitroCardHeaderView, NitroCardView } from '../../../../../layout'; +import { useRoomContext } from '../../../context/RoomContext'; +import { YoutubeVideoPlaybackStateEnum } from './utils/YoutubeVideoPlaybackStateEnum'; + +export const FurnitureYoutubeDisplayView: FC<{}> = props => +{ + const [objectId, setObjectId] = useState(-1); + const [videoId, setVideoId] = useState(null); + const [videoStart, setVideoStart] = useState(null); + const [videoEnd, setVideoEnd] = useState(null); + const [currentVideoState, setCurrentVideoState] = useState(-1); + const [selectedItem, setSelectedItem] = useState(null); + const [playlists, setPlaylists] = useState(null); + const [hasControl, setHasControl] = useState(false); + const [player, setPlayer] = useState(null); + const { eventDispatcher = null } = useRoomContext(); + + const onRoomWidgetUpdateYoutubeDisplayEvent = useCallback((event: RoomWidgetUpdateYoutubeDisplayEvent) => + { + switch(event.type) + { + case RoomWidgetUpdateYoutubeDisplayEvent.UPDATE_YOUTUBE_DISPLAY: { + setObjectId(event.objectId); + setHasControl(event.hasControl); + } + } + }, []); + + const close = useCallback(() => + { + setObjectId(-1); + setSelectedItem(null); + setPlaylists(null); + setHasControl(false); + setVideoId(null); + setVideoEnd(null); + setVideoStart(null); + setCurrentVideoState(-1); + }, []); + + CreateEventDispatcherHook(RoomWidgetUpdateYoutubeDisplayEvent.UPDATE_YOUTUBE_DISPLAY, eventDispatcher, onRoomWidgetUpdateYoutubeDisplayEvent); + + const onVideo = useCallback((event: YoutubeDisplayVideoMessageEvent) => + { + if(objectId === -1) return; + + const parser = event.getParser(); + + if(objectId !== parser.furniId) return; + + BatchUpdates(() => + { + setVideoId(parser.videoId); + setVideoStart(parser.startAtSeconds); + setVideoEnd(parser.endAtSeconds); + setCurrentVideoState(parser.state); + }); + }, [objectId]); + + const onPlaylists = useCallback((event: YoutubeDisplayPlaylistsEvent) => + { + if(objectId === -1) return; + + const parser = event.getParser(); + + if(objectId !== parser.furniId) return; + + BatchUpdates(() => + { + setPlaylists(parser.playlists); + setSelectedItem(parser.selectedPlaylistId); + setVideoId(null); + setCurrentVideoState(-1); + setVideoEnd(null); + setVideoStart(null); + }); + }, [objectId]); + + const onControlVideo = useCallback((event: YoutubeControlVideoMessageEvent) => + { + if(objectId === -1) return; + + const parser = event.getParser(); + + if(objectId !== parser.furniId) return; + + switch(parser.commandId) + { + case 1: + setCurrentVideoState(YoutubeVideoPlaybackStateEnum.PLAYING); + if(player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PLAYING) + player.playVideo(); + break; + case 2: + setCurrentVideoState(YoutubeVideoPlaybackStateEnum.PAUSED); + if(player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PAUSED) + player.pauseVideo(); + break; + } + }, [objectId, player]); + + CreateMessageHook(YoutubeDisplayVideoMessageEvent, onVideo); + CreateMessageHook(YoutubeDisplayPlaylistsEvent, onPlaylists); + CreateMessageHook(YoutubeControlVideoMessageEvent, onControlVideo); + + const processAction = useCallback((action: string) => + { + switch(action) + { + case 'playlist_prev': + SendMessageHook(new ControlYoutubeDisplayPlaybackMessageComposer(objectId, FurnitureYoutubeDisplayWidgetHandler.CONTROL_COMMAND_PREVIOUS_VIDEO)); + break; + case 'playlist_next': + SendMessageHook(new ControlYoutubeDisplayPlaybackMessageComposer(objectId, FurnitureYoutubeDisplayWidgetHandler.CONTROL_COMMAND_NEXT_VIDEO)); + break; + case 'video_pause': + if(hasControl && videoId && videoId.length) + { + SendMessageHook(new ControlYoutubeDisplayPlaybackMessageComposer(objectId, FurnitureYoutubeDisplayWidgetHandler.CONTROL_COMMAND_PAUSE_VIDEO)); + } + break; + case 'video_play': + if(hasControl && videoId && videoId.length) + { + SendMessageHook(new ControlYoutubeDisplayPlaybackMessageComposer(objectId, FurnitureYoutubeDisplayWidgetHandler.CONTROL_COMMAND_CONTINUE_VIDEO)); + } + break; + default: + if(selectedItem === action) + { + setSelectedItem(null); + SendMessageHook(new SetYoutubeDisplayPlaylistMessageComposer(objectId, '')); + return; + } + SendMessageHook(new SetYoutubeDisplayPlaylistMessageComposer(objectId, action)); + setSelectedItem(action); + } + }, [hasControl, objectId, selectedItem, videoId]); + + const onReady = useCallback((event: any) => + { + setPlayer(event.target); + }, []); + + const onStateChange = useCallback((event: any) => + { + setPlayer(event.target); + if(objectId) + { + switch(event.target.getPlayerState()) + { + case -1: + case 1: + if(currentVideoState === 2) + { + //event.target.pauseVideo(); + } + if(currentVideoState !== 1) + { + processAction('video_play'); + } + return; + case 2: + if(currentVideoState !== 2) + { + processAction('video_pause'); + } + } + } + }, [currentVideoState, objectId, processAction]); + + const getYoutubeOpts = useMemo( () => + { + if(!videoStart && !videoEnd) + { + return { + height: '375', + width: '500', + playerVars: { + autoplay: 1, + disablekb: 1, + controls: 0, + origin: window.origin, + modestbranding: 1 + } + } + } + + return { + height: '375', + width: '500', + playerVars: { + autoplay: 1, + disablekb: 1, + controls: 0, + origin: window.origin, + modestbranding: 1, + start: videoStart, + end: videoEnd + } + } + }, [videoEnd, videoStart]); + + if((objectId === -1)) return null; + + return ( + + + +
+
+ {(videoId && videoId.length > 0) && + + } + {(!videoId || videoId.length === 0) && +
{LocalizeText('widget.furni.video_viewer.no_videos')}
+ } +
+
+ + processAction('playlist_prev')} /> + processAction('playlist_next')} /> + +
{LocalizeText('widget.furni.video_viewer.playlists')}
+ + {playlists && playlists.map((entry, index) => + { + return ( + processAction(entry.video)} itemActive={entry.video === selectedItem}> + {entry.title} - {entry.description} + + ) + })} + +
+
+
+
+ ) +} diff --git a/src/views/room/widgets/furniture/youtube-tv/utils/YoutubeVideoPlaybackStateEnum.ts b/src/views/room/widgets/furniture/youtube-tv/utils/YoutubeVideoPlaybackStateEnum.ts new file mode 100644 index 00000000..3a885d15 --- /dev/null +++ b/src/views/room/widgets/furniture/youtube-tv/utils/YoutubeVideoPlaybackStateEnum.ts @@ -0,0 +1,9 @@ +export class YoutubeVideoPlaybackStateEnum +{ + public static readonly UNSTARTED = -1; + public static readonly ENDED = 0; + public static readonly PLAYING = 1; + public static readonly PAUSED = 2; + public static readonly BUFFERING = 3; + public static readonly CUED = 5; +}