Stage 2 Youtube & upgrade to vite 8

This commit is contained in:
duckietm
2026-04-09 15:35:58 +02:00
parent bbd4ccf30c
commit c7348a9509
5 changed files with 108 additions and 121 deletions
@@ -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 };
}, []); }, []);
+102 -113
View File
@@ -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>
+1 -4
View File
@@ -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;
} }
@@ -1097,4 +1094,4 @@
&.bubble-53 { &.bubble-53 {
background-image: url('@/assets/images/chat/chatbubbles/bubble_53.png'); background-image: url('@/assets/images/chat/chatbubbles/bubble_53.png');
} }
} }
-1
View File
@@ -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);