mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 15:36:18 +00:00
Stage 2 Youtube & upgrade to vite 8
This commit is contained in:
@@ -80,12 +80,10 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
|
|||||||
const windowHeight = elementRef.current.offsetHeight;
|
const windowHeight = elementRef.current.offsetHeight;
|
||||||
const viewportWidth = window.innerWidth;
|
const viewportWidth = window.innerWidth;
|
||||||
const viewportHeight = window.innerHeight;
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
const maxOutX = windowWidth * DRAG_OUTSIDE_PERCENT;
|
const maxOutX = windowWidth * DRAG_OUTSIDE_PERCENT;
|
||||||
const maxOutY = windowHeight * DRAG_OUTSIDE_PERCENT;
|
const maxOutY = windowHeight * DRAG_OUTSIDE_PERCENT;
|
||||||
|
|
||||||
const clampedX = Math.max(-maxOutX, Math.min(newX, viewportWidth - windowWidth + maxOutX));
|
const clampedX = Math.max(-maxOutX, Math.min(newX, viewportWidth - windowWidth + maxOutX));
|
||||||
const clampedY = Math.max(-maxOutY, Math.min(newY, viewportHeight - windowHeight + maxOutY));
|
const clampedY = Math.max(BOUNDS_THRESHOLD_TOP, Math.min(newY, viewportHeight - windowHeight + maxOutY));
|
||||||
|
|
||||||
return { x: clampedX, y: clampedY };
|
return { x: clampedX, y: clampedY };
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
import { ControlYoutubeDisplayPlaybackMessageComposer, YouTubeRoomBroadcastEvent, YouTubeRoomPlayComposer, YouTubeRoomWatchersEvent, YouTubeRoomWatchingComposer } from "@nitrots/nitro-renderer";
|
import { ControlYoutubeDisplayPlaybackMessageComposer, YouTubeRoomBroadcastEvent, YouTubeRoomPlayComposer, YouTubeRoomWatchersEvent, YouTubeRoomWatchingComposer } from "@nitrots/nitro-renderer";
|
||||||
import { FC, useEffect, useRef, useState } from "react";
|
import { FC, useEffect, useRef, useState } from "react";
|
||||||
import YouTube from "react-youtube";
|
import YouTube from "react-youtube";
|
||||||
import {
|
import { GetRoomSession, GetSessionDataManager, LocalizeText, SendMessageComposer, YoutubeVideoPlaybackStateEnum } from "../../api";
|
||||||
GetRoomSession,
|
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, LayoutAvatarImageView } from "../../common";
|
||||||
GetSessionDataManager,
|
|
||||||
SendMessageComposer,
|
|
||||||
YoutubeVideoPlaybackStateEnum,
|
|
||||||
} from "../../api";
|
|
||||||
import {
|
|
||||||
NitroCardContentView,
|
|
||||||
NitroCardHeaderView,
|
|
||||||
NitroCardView,
|
|
||||||
} from "../../common";
|
|
||||||
import { useFurnitureYoutubeWidget, useMessageEvent } from "../../hooks";
|
import { useFurnitureYoutubeWidget, useMessageEvent } from "../../hooks";
|
||||||
|
|
||||||
const CONTROL_COMMAND_PREVIOUS_VIDEO = 0;
|
const CONTROL_COMMAND_PREVIOUS_VIDEO = 0;
|
||||||
@@ -33,14 +24,7 @@ const extractVideoId = (input: string): string => {
|
|||||||
|
|
||||||
export const YouTubePlayerView: FC<{}> = () => {
|
export const YouTubePlayerView: FC<{}> = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [tab, setTab] = useState<
|
const [tab, setTab] = useState< | "player" | "playlist" | "spectators" | "settings" | "history" | "share" >("player");
|
||||||
| "player"
|
|
||||||
| "playlist"
|
|
||||||
| "spectators"
|
|
||||||
| "settings"
|
|
||||||
| "history"
|
|
||||||
| "share"
|
|
||||||
>("player");
|
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
const [isRoomMode, setIsRoomMode] = useState(false);
|
const [isRoomMode, setIsRoomMode] = useState(false);
|
||||||
const [volume, setVolume] = useState(100);
|
const [volume, setVolume] = useState(100);
|
||||||
@@ -53,65 +37,54 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
const [showVolumeSlider, setShowVolumeSlider] = useState(true);
|
const [showVolumeSlider, setShowVolumeSlider] = useState(true);
|
||||||
const playerRef = useRef<any>(null);
|
const playerRef = useRef<any>(null);
|
||||||
|
|
||||||
const {
|
const { objectId: youtubeObjectId, videoId: roomVideoId, currentVideoState, hasControl } = useFurnitureYoutubeWidget();
|
||||||
objectId: youtubeObjectId,
|
|
||||||
videoId: roomVideoId,
|
|
||||||
currentVideoState,
|
|
||||||
hasControl,
|
|
||||||
} = useFurnitureYoutubeWidget();
|
|
||||||
|
|
||||||
const [spectators, setSpectators] = useState<
|
const [spectators, setSpectators] = useState< { id: number; name: string; look: string }[] >([]);
|
||||||
{ id: number; name: string; look: string }[]
|
|
||||||
>([]);
|
|
||||||
// Room broadcast state: set when someone broadcasts a video to the room
|
|
||||||
const [broadcastVideo, setBroadcastVideo] = useState("");
|
const [broadcastVideo, setBroadcastVideo] = useState("");
|
||||||
const [broadcastSender, setBroadcastSender] = useState("");
|
const [broadcastSender, setBroadcastSender] = useState("");
|
||||||
const [broadcastPlaylist, setBroadcastPlaylist] = useState<string[]>([]);
|
const [broadcastPlaylist, setBroadcastPlaylist] = useState<string[]>([]);
|
||||||
const [watcherIds, setWatcherIds] = useState<Set<number>>(new Set());
|
const [watcherIds, setWatcherIds] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
// Listen for room-wide YouTube broadcast from the server
|
|
||||||
useMessageEvent<YouTubeRoomBroadcastEvent>(YouTubeRoomBroadcastEvent, event => {
|
useMessageEvent<YouTubeRoomBroadcastEvent>(YouTubeRoomBroadcastEvent, event => {
|
||||||
const parser = event.getParser();
|
const parser = event.getParser();
|
||||||
setBroadcastVideo(parser.videoId);
|
setBroadcastVideo(parser.videoId);
|
||||||
setBroadcastSender(parser.senderName);
|
setBroadcastSender(parser.senderName);
|
||||||
setBroadcastPlaylist(parser.playlist);
|
setBroadcastPlaylist(parser.playlist);
|
||||||
// Auto-open the player and load the broadcast video
|
|
||||||
if (parser.videoId) {
|
if (parser.videoId) {
|
||||||
setInputValue(parser.videoId);
|
setInputValue(parser.videoId);
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
setTab("player");
|
setTab("player");
|
||||||
|
} else {
|
||||||
|
setInputValue("");
|
||||||
|
setBroadcastVideo("");
|
||||||
|
setBroadcastSender("");
|
||||||
|
setBroadcastPlaylist([]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for updated watcher list from the server
|
useMessageEvent<YouTubeRoomWatchersEvent>(YouTubeRoomWatchersEvent, event => { setWatcherIds(new Set(event.getParser().watcherIds)); loadRoomUsers(); });
|
||||||
useMessageEvent<YouTubeRoomWatchersEvent>(YouTubeRoomWatchersEvent, event => {
|
|
||||||
setWatcherIds(new Set(event.getParser().watcherIds));
|
|
||||||
loadRoomUsers(); // refresh spectator list so we can mark watchers
|
|
||||||
});
|
|
||||||
|
|
||||||
// Notify server when we open/close the YouTube player
|
const sentWatchingRef = useRef(false);
|
||||||
|
const hasVideo = !!(inputValue && extractVideoId(inputValue));
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen && hasVideo && !sentWatchingRef.current) {
|
||||||
try { SendMessageComposer(new YouTubeRoomWatchingComposer(true)); } catch(e) {}
|
try { SendMessageComposer(new YouTubeRoomWatchingComposer(true)); } catch(e) {}
|
||||||
}
|
sentWatchingRef.current = true;
|
||||||
return () => {
|
} else if ((!isOpen || !hasVideo) && sentWatchingRef.current) {
|
||||||
try { SendMessageComposer(new YouTubeRoomWatchingComposer(false)); } catch(e) {}
|
try { SendMessageComposer(new YouTubeRoomWatchingComposer(false)); } catch(e) {}
|
||||||
};
|
sentWatchingRef.current = false;
|
||||||
}, [isOpen]);
|
}
|
||||||
|
}, [isOpen, hasVideo]);
|
||||||
|
|
||||||
// Enumerate room users via the session's userDataManager. Uses the
|
|
||||||
// same brute-force index scan that the old FurnitureYoutubeDisplayView
|
|
||||||
// used (and which worked). The fancier GetRoomEngine().getRoomObjects()
|
|
||||||
// approach doesn't reliably return objects when called from the toolbar
|
|
||||||
// context (outside the room widget tree).
|
|
||||||
const loadRoomUsers = () => {
|
const loadRoomUsers = () => {
|
||||||
try {
|
try {
|
||||||
const roomSession = GetRoomSession();
|
const roomSession = GetRoomSession();
|
||||||
if (!roomSession) { setSpectators([]); return; }
|
if (!roomSession) { setSpectators([]); return; }
|
||||||
const users: { id: number; name: string; look: string }[] = [];
|
const users: { id: number; name: string; look: string }[] = [];
|
||||||
|
const seen = new Set<number>();
|
||||||
for (let i = 0; i < 500; i++) {
|
for (let i = 0; i < 500; i++) {
|
||||||
const userData = roomSession.userDataManager.getUserDataByIndex(i);
|
const userData = roomSession.userDataManager.getUserDataByIndex(i);
|
||||||
if (userData && userData.name && userData.type === 1) {
|
if (userData && userData.name && userData.type === 1 && !seen.has(userData.userId)) {
|
||||||
|
seen.add(userData.userId);
|
||||||
users.push({ id: userData.userId, name: userData.name, look: userData.figure });
|
users.push({ id: userData.userId, name: userData.name, look: userData.figure });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,8 +94,6 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load room users when the player opens so the spectators count
|
|
||||||
// is visible on the tab button immediately.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) loadRoomUsers();
|
if (isOpen) loadRoomUsers();
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
@@ -139,10 +110,6 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
}, [youtubeObjectId, roomVideoId]);
|
}, [youtubeObjectId, roomVideoId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Hold the same handler reference for both add and remove. Using a
|
|
||||||
// fresh arrow function in the cleanup is a no-op because
|
|
||||||
// removeEventListener requires reference equality; every mount
|
|
||||||
// would otherwise leak a permanent listener on window.
|
|
||||||
const handler = () => setIsOpen((p) => !p);
|
const handler = () => setIsOpen((p) => !p);
|
||||||
window.addEventListener("youtube:toggle", handler);
|
window.addEventListener("youtube:toggle", handler);
|
||||||
return () => window.removeEventListener("youtube:toggle", handler);
|
return () => window.removeEventListener("youtube:toggle", handler);
|
||||||
@@ -154,7 +121,6 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(savedHistory);
|
const parsed = JSON.parse(savedHistory);
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed)) {
|
||||||
// Accept both legacy {id,title,...} objects and plain string[]
|
|
||||||
setHistory(parsed.map((entry: any) => typeof entry === "string" ? entry : entry?.id).filter(Boolean));
|
setHistory(parsed.map((entry: any) => typeof entry === "string" ? entry : entry?.id).filter(Boolean));
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
@@ -240,10 +206,10 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const videoId = extractVideoId(inputValue);
|
const videoId = extractVideoId(inputValue);
|
||||||
const isPlaying =
|
const isPlaying = currentVideoState === YoutubeVideoPlaybackStateEnum.PLAYING;
|
||||||
currentVideoState === YoutubeVideoPlaybackStateEnum.PLAYING;
|
|
||||||
const isPaused = currentVideoState === YoutubeVideoPlaybackStateEnum.PAUSED;
|
const isPaused = currentVideoState === YoutubeVideoPlaybackStateEnum.PAUSED;
|
||||||
const isMyRoom = GetSessionDataManager().isModerator || hasControl;
|
const roomSession = GetRoomSession();
|
||||||
|
const isMyRoom = GetSessionDataManager().isModerator || (roomSession && roomSession.isRoomOwner);
|
||||||
|
|
||||||
const QuickVolumeButton = ({
|
const QuickVolumeButton = ({
|
||||||
value,
|
value,
|
||||||
@@ -265,10 +231,10 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NitroCardView
|
<NitroCardView
|
||||||
className={`youtube-player-modal fixed ${isFullscreen ? "inset-0 w-full h-full z-[9999] rounded-none" : "top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[1000] w-[550px]"}`}
|
className={`youtube-player-modal ${isFullscreen ? "!fixed inset-0 w-full h-full z-[9999] rounded-none" : "w-[550px]"}`}
|
||||||
>
|
>
|
||||||
<NitroCardHeaderView
|
<NitroCardHeaderView
|
||||||
headerText={isRoomMode ? "📺 YouTube TV (Kamer)" : "▶ YouTube"}
|
headerText={isRoomMode ? "📺 YouTube TV" : "▶ YouTube"}
|
||||||
onCloseClick={() => setIsOpen(false)}
|
onCloseClick={() => setIsOpen(false)}
|
||||||
/>
|
/>
|
||||||
<NitroCardContentView>
|
<NitroCardContentView>
|
||||||
@@ -297,12 +263,12 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
>
|
>
|
||||||
📤
|
📤
|
||||||
</button>
|
</button>
|
||||||
{spectators.length > 0 && (
|
{watcherIds.size > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => { setTab("spectators"); loadRoomUsers(); }}
|
onClick={() => { setTab("spectators"); loadRoomUsers(); }}
|
||||||
className={`px-3 py-1 rounded text-sm ${tab === "spectators" ? "bg-amber-600 text-white" : "bg-gray-700 text-gray-300"}`}
|
className={`px-3 py-1 rounded text-sm ${tab === "spectators" ? "bg-amber-600 text-white" : "bg-gray-700 text-gray-300"}`}
|
||||||
>
|
>
|
||||||
👁️ {spectators.length}
|
📺 {watcherIds.size}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
@@ -318,22 +284,22 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
{isRoomMode && (
|
{isRoomMode && (
|
||||||
<div className="mb-2 p-2 bg-blue-900/50 rounded flex justify-between text-sm">
|
<div className="mb-2 p-2 bg-blue-900/50 rounded flex justify-between text-sm">
|
||||||
<span className="text-blue-300">
|
<span className="text-blue-300">
|
||||||
📺 Verbonden met YouTube TV
|
📺 Connected with YouTube TV
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{isPlaying && (
|
{isPlaying && (
|
||||||
<span className="text-green-400">
|
<span className="text-green-400">
|
||||||
▶ Speelt
|
▶ { LocalizeText('connection.login.play') }
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{isPaused && (
|
{isPaused && (
|
||||||
<span className="text-yellow-400">
|
<span className="text-yellow-400">
|
||||||
⏸ Gepauzeerd
|
⏸ { LocalizeText('wiredfurni.params.clock_control.3') }
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{isMyRoom && (
|
{isMyRoom && (
|
||||||
<span className="text-green-400 text-xs">
|
<span className="text-green-400 text-xs">
|
||||||
✓ Jij bent eigenaar
|
✓ { LocalizeText('navigator.filter.owner') }
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -360,7 +326,7 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-[280px] flex items-center justify-center bg-gray-800 text-gray-500">
|
<div className="h-[280px] flex items-center justify-center bg-gray-800 text-gray-500">
|
||||||
Geen video geladen
|
{ LocalizeText('widget.furni.video_viewer.no_videos') }
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -390,8 +356,23 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{broadcastVideo && broadcastSender && (
|
{broadcastVideo && broadcastSender && (
|
||||||
<div className="mt-2 p-2 bg-purple-900/50 rounded text-sm">
|
<div className="mt-2 p-2 bg-purple-900/50 rounded text-sm flex justify-between items-center">
|
||||||
<span className="text-purple-300">📡 {broadcastSender} speelt voor de kamer</span>
|
<span className="text-purple-300">📡 {broadcastSender} broadcasting</span>
|
||||||
|
{isMyRoom && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
try {
|
||||||
|
SendMessageComposer(new YouTubeRoomPlayComposer("", []));
|
||||||
|
} catch(e) {}
|
||||||
|
setBroadcastVideo("");
|
||||||
|
setBroadcastSender("");
|
||||||
|
setBroadcastPlaylist([]);
|
||||||
|
}}
|
||||||
|
className="px-2 py-0.5 bg-red-700 hover:bg-red-600 rounded text-white text-xs"
|
||||||
|
>
|
||||||
|
⏹ { LocalizeText('useproduct.widget.cancel') }
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -402,7 +383,7 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
disabled={!!broadcastVideo && !isMyRoom}
|
disabled={!!broadcastVideo && !isMyRoom}
|
||||||
className={`flex-1 p-2 rounded text-white text-sm ${(!!broadcastVideo && !isMyRoom) ? "bg-gray-800" : "bg-gray-700"}`}
|
className={`flex-1 p-2 rounded text-white text-sm ${(!!broadcastVideo && !isMyRoom) ? "bg-gray-800" : "bg-gray-700"}`}
|
||||||
placeholder="YouTube URL of video ID"
|
placeholder="YouTube URL / video ID"
|
||||||
/>
|
/>
|
||||||
{isMyRoom && videoId && (
|
{isMyRoom && videoId && (
|
||||||
<button
|
<button
|
||||||
@@ -414,7 +395,7 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
className="px-3 bg-purple-600 rounded text-white text-sm whitespace-nowrap"
|
className="px-3 bg-purple-600 rounded text-white text-sm whitespace-nowrap"
|
||||||
title="Speel deze video voor iedereen in de kamer"
|
title="Speel deze video voor iedereen in de kamer"
|
||||||
>
|
>
|
||||||
📡 Kamer
|
📡 { LocalizeText('wiredchests.logs.type.1') }
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -428,7 +409,7 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
placeholder="Video URL toevoegen..."
|
placeholder="Add video URL..."
|
||||||
className="flex-1 p-2 bg-gray-700 text-white rounded text-sm"
|
className="flex-1 p-2 bg-gray-700 text-white rounded text-sm"
|
||||||
onKeyDown={(e) =>
|
onKeyDown={(e) =>
|
||||||
e.key === "Enter" && addToPlaylist()
|
e.key === "Enter" && addToPlaylist()
|
||||||
@@ -446,18 +427,18 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
onClick={() => setInputValue("")}
|
onClick={() => setInputValue("")}
|
||||||
className="flex-1 px-3 py-2 bg-gray-700 rounded text-white text-sm"
|
className="flex-1 px-3 py-2 bg-gray-700 rounded text-white text-sm"
|
||||||
>
|
>
|
||||||
🔄 Nieuwe video
|
🔄 New video
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPlaylist([])}
|
onClick={() => setPlaylist([])}
|
||||||
className="px-3 py-2 bg-red-900 rounded text-white text-sm"
|
className="px-3 py-2 bg-red-900 rounded text-white text-sm"
|
||||||
>
|
>
|
||||||
🗑 Leeg
|
🗑 Clear
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{playlist.length === 0 ? (
|
{playlist.length === 0 ? (
|
||||||
<div className="p-4 text-center text-gray-500 text-sm">
|
<div className="p-4 text-center text-gray-500 text-sm">
|
||||||
Playlist is leeg
|
Playlist is empty
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-[250px] overflow-y-auto space-y-1">
|
<div className="max-h-[250px] overflow-y-auto space-y-1">
|
||||||
@@ -498,18 +479,18 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="text-gray-400 text-sm">
|
<div className="text-gray-400 text-sm">
|
||||||
🕐 Bekeken video's ({history.length})
|
🕐 Watch history ({history.length})
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setHistory([])}
|
onClick={() => setHistory([])}
|
||||||
className="text-red-400 text-xs hover:text-red-300"
|
className="text-red-400 text-xs hover:text-red-300"
|
||||||
>
|
>
|
||||||
🗑 Wissen
|
🗑 Clear
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{history.length === 0 ? (
|
{history.length === 0 ? (
|
||||||
<div className="p-4 text-center text-gray-500 text-sm">
|
<div className="p-4 text-center text-gray-500 text-sm">
|
||||||
Nog geen video's bekeken
|
No videos watched yet
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-[300px] overflow-y-auto space-y-1">
|
<div className="max-h-[300px] overflow-y-auto space-y-1">
|
||||||
@@ -536,7 +517,7 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="p-3 bg-gray-800 rounded">
|
<div className="p-3 bg-gray-800 rounded">
|
||||||
<div className="text-gray-400 text-sm mb-2">
|
<div className="text-gray-400 text-sm mb-2">
|
||||||
📤 Video delen
|
📤 Share video
|
||||||
</div>
|
</div>
|
||||||
{videoId ? (
|
{videoId ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -561,13 +542,13 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-gray-500 text-sm text-center py-4">
|
<div className="text-gray-500 text-sm text-center py-4">
|
||||||
Selecteer eerst een video om te delen
|
Select a video first to share
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-gray-800 rounded">
|
<div className="p-3 bg-gray-800 rounded">
|
||||||
<div className="text-gray-400 text-sm mb-2">
|
<div className="text-gray-400 text-sm mb-2">
|
||||||
📋 Snel delen
|
📋 Quick share
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -576,19 +557,19 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
`📺 https://youtube.com/watch?v=${videoId}`,
|
`📺 https://youtube.com/watch?v=${videoId}`,
|
||||||
);
|
);
|
||||||
alert("Gekopieerd naar clipboard!");
|
alert("Copied to clipboard!");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!videoId}
|
disabled={!videoId}
|
||||||
className="px-3 py-2 bg-gray-700 rounded text-white text-sm disabled:opacity-50"
|
className="px-3 py-2 bg-gray-700 rounded text-white text-sm disabled:opacity-50"
|
||||||
>
|
>
|
||||||
📋 Copy met emoji
|
📋 Copy with emoji
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (videoId) {
|
if (videoId) {
|
||||||
const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(
|
const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(
|
||||||
`Nu kijken: https://youtube.com/watch?v=${videoId}`,
|
'Now watching: https://youtube.com/watch?v=${videoId}',
|
||||||
)}`;
|
)}`;
|
||||||
window.open(url, "_blank");
|
window.open(url, "_blank");
|
||||||
}
|
}
|
||||||
@@ -603,11 +584,22 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === "spectators" && (
|
{tab === "spectators" && (() => {
|
||||||
|
const watchers: { id: number; name: string; look: string }[] = [];
|
||||||
|
const rs = GetRoomSession();
|
||||||
|
if (rs) {
|
||||||
|
for (const uid of watcherIds) {
|
||||||
|
const ud = rs.userDataManager.getUserData(uid);
|
||||||
|
if (ud && ud.name) {
|
||||||
|
watchers.push({ id: ud.userId, name: ud.name, look: ud.figure });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
<div className="p-3 bg-gray-800 rounded">
|
<div className="p-3 bg-gray-800 rounded">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<div className="text-gray-400 text-sm">
|
<div className="text-gray-400 text-sm">
|
||||||
👁️ Gebruikers in kamer ({spectators.length})
|
📺 {watchers.length} watching
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={loadRoomUsers}
|
onClick={loadRoomUsers}
|
||||||
@@ -616,40 +608,31 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
🔄
|
🔄
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{spectators.length === 0 ? (
|
{watchers.length === 0 ? (
|
||||||
<div className="text-gray-500 text-sm text-center py-4">
|
<div className="text-gray-500 text-sm text-center py-4">
|
||||||
Geen gebruikers in deze kamer
|
No one is watching
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-[200px] overflow-y-auto space-y-1">
|
<div className="max-h-[200px] overflow-y-auto space-y-1">
|
||||||
{spectators.map((user) => (
|
{watchers.map((user) => (
|
||||||
<div
|
<div
|
||||||
key={user.id}
|
key={user.id}
|
||||||
className="flex items-center gap-2 p-2 bg-gray-700 rounded"
|
className="flex items-center gap-2 p-2 bg-gray-700 rounded"
|
||||||
>
|
>
|
||||||
<img
|
<div className="shrink-0 overflow-hidden">
|
||||||
src={`https://www.habbo.com/habbo-imaging/avatarimage?figure=${user.look}&size=s&direction=2&head_direction=2`}
|
<LayoutAvatarImageView figure={user.look} headOnly direction={2} scale={1} className="!w-[45px] !h-[65px] -mt-[5px] -ml-[5px]" />
|
||||||
alt={user.name}
|
</div>
|
||||||
className="w-8 h-8 rounded"
|
|
||||||
onError={(e) => {
|
|
||||||
(
|
|
||||||
e.target as HTMLImageElement
|
|
||||||
).src =
|
|
||||||
"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 36 36'><circle cx='18' cy='18' r='18' fill='%23888'/></svg>";
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="text-white text-sm flex-1">
|
<span className="text-white text-sm flex-1">
|
||||||
{user.name}
|
{user.name}
|
||||||
</span>
|
</span>
|
||||||
{watcherIds.has(user.id) && (
|
<span className="text-amber-400 text-xs">📺</span>
|
||||||
<span className="text-amber-400 text-xs" title="Kijkt YouTube">📺</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{tab === "settings" && (
|
{tab === "settings" && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -701,7 +684,7 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
}
|
}
|
||||||
className="w-4 h-4"
|
className="w-4 h-4"
|
||||||
/>
|
/>
|
||||||
🔇 Dempen
|
🔇 Mute
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2 text-white text-sm cursor-pointer">
|
<label className="flex items-center gap-2 text-white text-sm cursor-pointer">
|
||||||
<input
|
<input
|
||||||
@@ -712,7 +695,7 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
}
|
}
|
||||||
className="w-4 h-4"
|
className="w-4 h-4"
|
||||||
/>
|
/>
|
||||||
🔁 Herhalen
|
🔁 Loop
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2 text-white text-sm cursor-pointer">
|
<label className="flex items-center gap-2 text-white text-sm cursor-pointer">
|
||||||
<input
|
<input
|
||||||
@@ -730,14 +713,20 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
<div className="p-2 bg-gray-800 rounded text-xs text-gray-400">
|
<div className="p-2 bg-gray-800 rounded text-xs text-gray-400">
|
||||||
<div className="font-bold mb-1">ℹ️ Info</div>
|
<div className="font-bold mb-1">ℹ️ Info</div>
|
||||||
<div>
|
<div>
|
||||||
Room Mode:{" "}
|
📡 Broadcast:{" "}
|
||||||
{isRoomMode ? "✓ Actief" : "✕ Niet actief"}
|
{broadcastVideo
|
||||||
|
? <span className="text-green-400">✓ Active ({broadcastSender} playing)</span>
|
||||||
|
: <span className="text-gray-500">✕ No video</span>}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Controle:{" "}
|
🎮 Controle:{" "}
|
||||||
{hasControl
|
{isMyRoom
|
||||||
? "✓ Je hebt controle"
|
? <span className="text-green-400">✓ You are the owner</span>
|
||||||
: "✕ Alleen kijken"}
|
: <span className="text-gray-500">✕ Viewing only</span>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
👁️ Viewers:{" "}
|
||||||
|
<span className="text-amber-400">{watcherIds.size}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
&.type-0 {
|
&.type-0 {
|
||||||
|
|
||||||
// normal
|
|
||||||
.message {
|
.message {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
@@ -17,7 +16,6 @@
|
|||||||
|
|
||||||
&.type-1 {
|
&.type-1 {
|
||||||
|
|
||||||
// whisper
|
|
||||||
.message {
|
.message {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
@@ -27,7 +25,6 @@
|
|||||||
|
|
||||||
&.type-2 {
|
&.type-2 {
|
||||||
|
|
||||||
// shout
|
|
||||||
.message {
|
.message {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,6 @@
|
|||||||
|
|
||||||
&.stickie-yellow {
|
&.stickie-yellow {
|
||||||
background-image: url('@/assets/images/room-widgets/stickie-widget/stickie-yellow.png');
|
background-image: url('@/assets/images/room-widgets/stickie-widget/stickie-yellow.png');
|
||||||
//background-position: -191px -184px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.stickie-green {
|
&.stickie-green {
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ const useNotificationState = () =>
|
|||||||
{
|
{
|
||||||
const parser = event.getParser();
|
const parser = event.getParser();
|
||||||
|
|
||||||
|
// Skip if AchievementNotificationMessageEvent already showed a notification for this badge
|
||||||
if(recentBadgeNotifications.has(parser.badgeCode)) return;
|
if(recentBadgeNotifications.has(parser.badgeCode)) return;
|
||||||
|
|
||||||
recentBadgeNotifications.add(parser.badgeCode);
|
recentBadgeNotifications.add(parser.badgeCode);
|
||||||
@@ -233,6 +234,9 @@ const useNotificationState = () =>
|
|||||||
|
|
||||||
const badgeName = LocalizeBadgeName(parser.badgeCode);
|
const badgeName = LocalizeBadgeName(parser.badgeCode);
|
||||||
const badgeImage = GetSessionDataManager().getBadgeUrl(parser.badgeCode);
|
const badgeImage = GetSessionDataManager().getBadgeUrl(parser.badgeCode);
|
||||||
|
// senderName is non-empty only when a staff member awarded the badge
|
||||||
|
// via the `:badge` command. Empty for achievements, catalog buys,
|
||||||
|
// wired rewards, poll rewards, etc.
|
||||||
const senderName = parser.senderName || '';
|
const senderName = parser.senderName || '';
|
||||||
|
|
||||||
showSingleBubble(badgeName, NotificationBubbleType.BADGE_RECEIVED, badgeImage, parser.badgeCode, senderName);
|
showSingleBubble(badgeName, NotificationBubbleType.BADGE_RECEIVED, badgeImage, parser.badgeCode, senderName);
|
||||||
|
|||||||
Reference in New Issue
Block a user