From 29951956a7a351168eca99152c30b0f94071d3d5 Mon Sep 17 00:00:00 2001 From: Ipmake Date: Thu, 23 Jan 2025 16:57:48 +0100 Subject: [PATCH] feat: Add playback state management and user connection handling in sync --- backend/src/common/sync.ts | 51 +++- backend/src/types.ts | 15 +- frontend/src/App.tsx | 2 + frontend/src/common/NumberExtra.ts | 3 + frontend/src/components/AppBar.tsx | 51 ++-- frontend/src/components/PerPlexedSync.tsx | 4 +- frontend/src/components/ToastManager.tsx | 302 ++++++++++++++++++++++ frontend/src/index.tsx | 11 +- frontend/src/pages/WaitingRoom.tsx | 11 +- frontend/src/pages/Watch.tsx | 140 +++++++++- frontend/src/states/SyncSessionState.ts | 90 ++++++- frontend/src/types.d.ts | 33 ++- 12 files changed, 664 insertions(+), 49 deletions(-) create mode 100644 frontend/src/common/NumberExtra.ts create mode 100644 frontend/src/components/ToastManager.tsx diff --git a/backend/src/common/sync.ts b/backend/src/common/sync.ts index 3697045..65740fd 100644 --- a/backend/src/common/sync.ts +++ b/backend/src/common/sync.ts @@ -79,6 +79,15 @@ io?.on('connection', async (socket) => { host: isHost } satisfies PerPlexed.Sync.Ready); + io?.to(room).emit('EVNT_USER_JOIN', { + uid: user.uuid, + socket: socket.id, + name: user.friendlyName, + avatar: user.thumb + } satisfies PerPlexed.Sync.Member); + + AddEvents(socket, isHost, room); + socket.on('disconnect', () => { console.log(`SYNC [${socket.id}] disconnected`); @@ -95,12 +104,52 @@ io?.on('connection', async (socket) => { io?.sockets.sockets.get(client)?.disconnect(); }); } + } else { + io?.to(room).emit('EVNT_USER_LEAVE', { + uid: user.uuid, + socket: socket.id, + name: user.friendlyName, + avatar: user.thumb + } satisfies PerPlexed.Sync.Member); } }); }) -function AddEvents(socket: Socket, isHost: boolean) { +function AddEvents(socket: Socket, isHost: boolean, room: string) { + const user = socket.data.user as PerPlexed.PlexTV.User; + + socket.onAny((event, ...args) => { + if(!event.startsWith('SYNC_')) return; + + console.log(`SYNC [${socket.id}] emitting HOST ${event} to ${room}`); + io?.to(room).emit(`HOST_${event}`, ...args); + }); + + socket.onAny((event, ...args) => { + if(!isHost) return; + if(!event.startsWith('RES_')) return; + + console.log(`SYNC [${socket.id}] emitting ${event} to ${room}`); + io?.to(room).emit(`${event}`, { + uid: user.uuid, + socket: socket.id, + name: user.friendlyName, + avatar: user.thumb + } satisfies PerPlexed.Sync.Member, ...args); + }); + socket.onAny((event, ...args) => { + if(!event.startsWith("EVNT_")) return; + + console.log(`SYNC [${socket.id}] emitting EVENT ${event} to ${room}`); + io?.to(room).emit(`${event}`, { + uid: user.uuid, + socket: socket.id, + name: user.friendlyName, + avatar: user.thumb + } satisfies PerPlexed.Sync.Member, ...args); + }) + } function GenerateRoomID() { diff --git a/backend/src/types.ts b/backend/src/types.ts index 59ffabe..acce00e 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -7,7 +7,7 @@ export namespace PerPlexed { export namespace Sync { export interface SocketError { - type: string; + type: string; message: string; } @@ -15,6 +15,19 @@ export namespace PerPlexed { room: string; host: boolean; } + + export interface PlayBackState { + key?: string; + state: string; + time?: number; + } + + export interface Member { + uid: string; + socket: string; + name: string; + avatar: string; + } } export namespace PlexTV { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 12b2a11..2f78e7d 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,6 +14,7 @@ import { useWatchListCache } from "./states/WatchListCache"; import Startup, { useStartupState } from "./pages/Startup"; import PerPlexedSync from "./components/PerPlexedSync"; import WaitingRoom from "./pages/WaitingRoom"; +import ToastManager from "./components/ToastManager"; function AppManager() { const { loading } = useStartupState(); @@ -64,6 +65,7 @@ function App() { <> + } /> } /> diff --git a/frontend/src/common/NumberExtra.ts b/frontend/src/common/NumberExtra.ts new file mode 100644 index 0000000..c0abef5 --- /dev/null +++ b/frontend/src/common/NumberExtra.ts @@ -0,0 +1,3 @@ +export function absoluteDifference(num1: number, num2: number): number { + return Math.abs(num1 - num2); +} \ No newline at end of file diff --git a/frontend/src/components/AppBar.tsx b/frontend/src/components/AppBar.tsx index 6f2233f..93ea1ce 100755 --- a/frontend/src/components/AppBar.tsx +++ b/frontend/src/components/AppBar.tsx @@ -29,6 +29,7 @@ import { getAllLibraries, getSearch, getTranscodeImageURL } from "../plex"; import MetaScreen from "./MetaScreen"; import { useUserSessionStore } from "../states/UserSession"; import { + Favorite, Fullscreen, Logout, People, @@ -37,6 +38,7 @@ import { } from "@mui/icons-material"; import { useSyncInterfaceState } from "./PerPlexedSync"; import { useSyncSessionState } from "../states/SyncSessionState"; +import { config } from ".."; const BarSide: SxProps = { display: "flex", @@ -109,8 +111,7 @@ function Appbar() { vertical: "top", horizontal: "center", }} - sx={{ - }} + sx={{}} > { - useSyncInterfaceState.getState().setOpen(true); - setAnchorEl(null) + setAnchorEl(null); + window.open("https://github.com/sponsors/Ipmake", "_blank"); }} > - + - Watch2Gether + Support + {!config.DISABLE_PERPLEXED_SYNC && ( + { + useSyncInterfaceState.getState().setOpen(true); + setAnchorEl(null); + }} + > + + + + Watch2Gether + + )} + { // toggle Fullscreen @@ -153,7 +168,7 @@ function Appbar() { { - setAnchorEl(null) + setAnchorEl(null); // navigate("/settings"); }} disabled @@ -389,15 +404,15 @@ function SearchBar() { break; case "ArrowDown": e.preventDefault(); - if(searchResults.length === 0) return; + if (searchResults.length === 0) return; setSelectedIndex((prev) => prev === null ? 0 : Math.min(prev + 1, searchResults.length - 1) ); break; case "ArrowUp": e.preventDefault(); - if(searchResults.length === 0) return; - if(selectedIndex === 0) return setSelectedIndex(null); + if (searchResults.length === 0) return; + if (selectedIndex === 0) return setSelectedIndex(null); setSelectedIndex((prev) => prev === null ? 0 : Math.max(prev - 1, 0) @@ -405,21 +420,25 @@ function SearchBar() { break; case "Tab": e.preventDefault(); - if(searchResults.length === 0) return; + if (searchResults.length === 0) return; // if it gets to the last item, then set to null - if(selectedIndex === searchResults.length - 1) return setSelectedIndex(null); + if (selectedIndex === searchResults.length - 1) + return setSelectedIndex(null); setSelectedIndex((prev) => prev === null ? 0 : Math.min(prev + 1, searchResults.length - 1) ); break; case "Enter": - if(searchValue.length === 0) return; + if (searchValue.length === 0) return; if (selectedIndex !== null && searchResults.length > 0) { if (searchResults[selectedIndex].Metadata?.ratingKey) { - setSearchParams(new URLSearchParams({ - mid: searchResults[selectedIndex].Metadata?.ratingKey || '', - })); + setSearchParams( + new URLSearchParams({ + mid: + searchResults[selectedIndex].Metadata?.ratingKey || "", + }) + ); } else if (searchResults[selectedIndex].Directory) { navigate( `/library/${searchResults[selectedIndex].Directory?.librarySectionID}/dir/genre/${searchResults[selectedIndex].Directory?.id}` diff --git a/frontend/src/components/PerPlexedSync.tsx b/frontend/src/components/PerPlexedSync.tsx index 626fd5f..caafa17 100644 --- a/frontend/src/components/PerPlexedSync.tsx +++ b/frontend/src/components/PerPlexedSync.tsx @@ -240,7 +240,7 @@ function PerPlexedSync() { setPage("load"); const res = await useSyncSessionState .getState() - .connect(inputRoom); + .connect(inputRoom, navigate); console.log(res); @@ -292,7 +292,7 @@ function PerPlexedSync() { color="primary" onClick={async () => { setPage("load"); - const res = await useSyncSessionState.getState().connect(); + const res = await useSyncSessionState.getState().connect(undefined, navigate); if (res !== true) { setError(res.message); setPage("home"); diff --git a/frontend/src/components/ToastManager.tsx b/frontend/src/components/ToastManager.tsx new file mode 100644 index 0000000..bcdb937 --- /dev/null +++ b/frontend/src/components/ToastManager.tsx @@ -0,0 +1,302 @@ +import { + PersonAdd, + PersonRemove, + PlayArrowRounded, + PlaylistAddRounded, + Pause, + RedoRounded, + ResetTv +} from "@mui/icons-material"; +import { Avatar, Box, Divider, Typography } from "@mui/material"; +import { create } from "zustand"; +import { useEffect, useState } from "react"; +import React from "react"; + +export interface ToastState { + toasts: ToastProps[]; + addToast: ( + user: PerPlexed.Sync.Member, + icon: ToastIcons, + message: string, + duration: number + ) => void; +} + +export const useToast = create((set) => ({ + toasts: [], + addToast: ( + user: PerPlexed.Sync.Member, + icon: ToastIcons, + message: string, + duration: number = 5000 + ) => { + const toastID = Math.random() * 1000; + + set((state) => ({ + toasts: [ + ...state.toasts, + { + id: toastID, + user, + icon, + message, + duration, + toRemove: false, + }, + ], + })); + }, +})); + +interface ToastProps { + id: number; + duration: number; + message: string; + user: PerPlexed.Sync.Member; + icon: ToastIcons; + toRemove?: boolean; +} + +function ToastManager() { + const { toasts } = useToast(); + + useEffect(() => { + const interval = setInterval(() => { + let toasts = useToast.getState().toasts; + + if (!toasts.length) return; + + // only filter if all toasts are to be removed + if (toasts.every((toast) => toast.toRemove)) { + toasts = toasts.filter((toast) => !toast.toRemove); + + useToast.setState({ + toasts, + }); + } + }, 500); + return () => clearInterval(interval); + }, []); + + return ( + + {toasts.map((toast, index) => ( + + ))} + + ); +} + +export default ToastManager; + +export type ToastIcons = + | "Play" + | "Pause" + | "UserAdd" + | "UserRemove" + | "PlaySet"; + +export function Toast({ + id, + user, + icon, + message, + duration = 5000, +}: { + id: number; + user: PerPlexed.Sync.Member; + icon: ToastIcons; + message: string; + duration?: number; +}) { + /* + * Animation States: + * 0 - Slide in + * 1 - Static + * 2 - Slide out + */ + const [animationState, setAnimationState] = useState(0); + + useEffect(() => { + setTimeout(() => { + setAnimationState(1); + }, 500); + + setTimeout(() => { + setAnimationState(2); + + setTimeout(() => { + console.log(id); + + let toasts = useToast.getState().toasts; + + toasts = toasts.map((toast) => { + if (toast.id === id) { + toast.toRemove = true; + } + + return toast; + }); + + useToast.setState({ + toasts, + }); + }, 500); + }, duration); + }, []); + + return ( + + {icon === "Play" && ( + + )} + + {icon === "Pause" && ( + + )} + + {icon === "PlaySet" && ( + + )} + + {icon === "UserAdd" && ( + + )} + + {icon === "UserRemove" && ( + + )} + + + + + + + + + {user.name} + + + + + {message} + + + + ); +} diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 2d78bda..d1bc8f0 100755 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -17,15 +17,14 @@ if(!localStorage.getItem("clientID")) localStorage.setItem("clientID", makeid(24 sessionStorage.setItem("sessionID", uuidv4()); -interface ConfigInterface { - [key: string]: any; -} - -let config: ConfigInterface; +let config: PerPlexed.ConfigOptions = { + DISABLE_PROXY: false, + DISABLE_PERPLEXED_SYNC: false +}; (() => { if(!localStorage.getItem("config")) return - config = JSON.parse(localStorage.getItem("config") as string) as ConfigInterface; + config = JSON.parse(localStorage.getItem("config") as string) as PerPlexed.ConfigOptions; })(); if(!localStorage.getItem("quality")) localStorage.setItem("quality", "12000"); diff --git a/frontend/src/pages/WaitingRoom.tsx b/frontend/src/pages/WaitingRoom.tsx index 7c36a64..188f653 100644 --- a/frontend/src/pages/WaitingRoom.tsx +++ b/frontend/src/pages/WaitingRoom.tsx @@ -7,7 +7,7 @@ import { useNavigate } from "react-router-dom"; function WaitingRoom() { const [loading, setLoading] = React.useState(true); - const { room, isHost } = useSyncSessionState(); + const { room, isHost, socket } = useSyncSessionState(); const { setOpen } = useSyncInterfaceState(); const navigate = useNavigate(); @@ -15,6 +15,15 @@ function WaitingRoom() { if (isHost || !room) navigate("/"); }, [room, isHost, navigate]); + useEffect(() => { + if(!socket) return; + + socket.once("RES_SYNC_RESYNC_PLAYBACK", (user, data: PerPlexed.Sync.PlayBackState) => { + console.log("Playback resync received", data); + navigate(`/watch/${data.key}?t=${data.time}`); + }) + }, [navigate, socket]); + return ( (false); + const { room, socket, isHost } = useSyncSessionState(); + const { open: syncInterfaceOpen, setOpen: setSyncInterfaceOpen } = + useSyncInterfaceState(); + const loadMetadata = async (itemID: string) => { await getUniversalDecision(itemID, { maxVideoBitrate: quality.bitrate, @@ -192,10 +203,82 @@ function Watch() { await sendUniversalPing(); }, 10000); + if (itemID && isHost) + socket?.emit("RES_SYNC_SET_PLAYBACK", { + key: itemID, + state: playing ? "playing" : "paused", + time: player.current?.getCurrentTime() ?? 0, + } satisfies PerPlexed.Sync.PlayBackState); + return () => { clearInterval(interval); }; - }, [itemID]); + }, [isHost, itemID, socket]); + + useEffect(() => { + if (!socket || !room) return; + + const resyncInterval = setInterval(async () => { + if (!itemID || !socket || !isHost) return; + + socket.emit("RES_SYNC_RESYNC_PLAYBACK", { + key: itemID, + state: playing ? "playing" : "paused", + time: player.current?.getCurrentTime() ?? 0, + } satisfies PerPlexed.Sync.PlayBackState); + }, 2500); + + const resyncPlayback = async (data: PerPlexed.Sync.PlayBackState) => { + if (data.key !== itemID) { + navigate(`/watch/${data.key}?t=${data.time}`); + return; + } + + if (data.time) { + const dif = absoluteDifference( + player.current?.getCurrentTime() ?? 0, + data.time + ); + + if (dif > 2) player.current?.seekTo(data.time, "seconds"); + } + + if (data.state === "playing") setPlaying(true); + if (data.state === "paused") setPlaying(false); + }; + + const endPlayback = async () => { + navigate("/sync/waitingroom") + } + + const pausePlayback = async () => { + setPlaying(false); + }; + const resumePlayback = async () => { + setPlaying(true); + }; + const seekPlayback = async (time: number) => { + player.current?.seekTo(time, "seconds"); + }; + + if (!isHost) SessionStateEmitter.on("PLAYBACK_RESYNC", resyncPlayback); + if (!isHost) SessionStateEmitter.on("PLAYBACK_END", endPlayback); + + SessionStateEmitter.on("PLAYBACK_PAUSE", pausePlayback); + SessionStateEmitter.on("PLAYBACK_RESUME", resumePlayback); + SessionStateEmitter.on("PLAYBACK_SEEK", seekPlayback); + + return () => { + SessionStateEmitter.off("PLAYBACK_RESYNC", resyncPlayback); + SessionStateEmitter.off("PLAYBACK_END", endPlayback); + + SessionStateEmitter.off("PLAYBACK_PAUSE", pausePlayback); + SessionStateEmitter.off("PLAYBACK_RESUME", resumePlayback); + SessionStateEmitter.off("PLAYBACK_SEEK", seekPlayback); + + clearInterval(resyncInterval); + }; + }, [isHost, itemID, navigate, playing, room, socket]); useEffect(() => { if (!itemID) return; @@ -216,13 +299,14 @@ function Watch() { if (terminationCode) { setShowError(`${terminationCode} - ${terminationText}`); setPlaying(false); + socket?.emit("EVNT_SYNC_PAUSE"); } }; const updateInterval = setInterval(updateTimeline, 5000); return () => clearInterval(updateInterval); - }, [buffering, itemID, playing]); + }, [buffering, itemID, playing, socket]); useEffect(() => { // set css style for .ui-video-seek-slider .track .main .connect @@ -271,8 +355,18 @@ function Watch() { useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { const actions: { [key: string]: () => void } = { - " ": () => setPlaying((state) => !state), - k: () => setPlaying((state) => !state), + " ": () => + setPlaying((state) => { + if (state) socket?.emit("EVNT_SYNC_PAUSE"); + else socket?.emit("EVNT_SYNC_RESUME"); + return !state; + }), + k: () => + setPlaying((state) => { + if (state) socket?.emit("EVNT_SYNC_PAUSE"); + else socket?.emit("EVNT_SYNC_RESUME"); + return !state; + }), j: () => player.current?.seekTo(player.current.getCurrentTime() - 10), l: () => player.current?.seekTo(player.current.getCurrentTime() + 10), s: () => { @@ -347,7 +441,7 @@ function Watch() { return () => { document.removeEventListener("keydown", handleKeyDown); }; - }, [metadata, navigate, playQueue]); + }, [metadata, navigate, playQueue, socket]); return ( <> @@ -1080,6 +1174,7 @@ function Watch() { mountOnEnter unmountOnExit in={ + (room ? isHost : true) && metadata.Marker && metadata.Marker.filter( (marker) => @@ -1158,6 +1253,7 @@ function Watch() { mountOnEnter unmountOnExit in={ + (room ? isHost : true) && metadata.Marker && metadata.Marker.filter( (marker) => @@ -1238,6 +1334,7 @@ function Watch() { mountOnEnter unmountOnExit in={ + (room ? isHost : true) && metadata.Marker && metadata.Marker.filter( (marker) => @@ -1367,6 +1464,9 @@ function Watch() { > { + if(room && !isHost) socket?.disconnect(); + if(room && isHost) socket?.emit("RES_SYNC_PLAYBACK_END"); + if (itemID && player.current) getTimelineUpdate( parseInt(itemID), @@ -1435,6 +1535,7 @@ function Watch() { bufferTime={buffered * 1000} onChange={(value) => { player.current?.seekTo(value / 1000); + socket?.emit("EVNT_SYNC_SEEK", value / 1000); }} getPreviewScreenUrl={(value) => { if ( @@ -1494,6 +1595,8 @@ function Watch() { { setPlaying(!playing); + if (playing) socket?.emit("EVNT_SYNC_PAUSE"); + else socket?.emit("EVNT_SYNC_RESUME"); }} > {playing ? ( @@ -1594,6 +1697,16 @@ function Watch() { + {room && ( + { + setSyncInterfaceOpen(true); + }} + > + + + )} + { if (!document.fullscreenElement) @@ -1617,12 +1730,17 @@ function Watch() { switch (e.detail) { case 1: - setPlaying((state) => !state); + setPlaying((state) => { + if (state) socket?.emit("EVNT_SYNC_PAUSE"); + else socket?.emit("EVNT_SYNC_RESUME"); + return !state; + }); break; case 2: if (!document.fullscreenElement) { document.documentElement.requestFullscreen(); setPlaying(true); + socket?.emit("EVNT_SYNC_RESUME"); } else document.exitFullscreen(); break; default: @@ -1671,6 +1789,7 @@ function Watch() { // window.location.reload(); setPlaying(false); + socket?.emit("EVNT_SYNC_PAUSE"); if (showError) return; // filter out links from the error messages @@ -1695,17 +1814,21 @@ function Watch() { }, }} onEnded={() => { + if (room && !isHost) return; if (!playQueue) return console.log("No play queue"); - if (metadata.type !== "episode") + if (metadata.type !== "episode") { + if (room && isHost) socket?.emit("RES_SYNC_PLAYBACK_END"); return navigate( `/browse/${metadata.librarySectionID}?${queryBuilder({ mid: metadata.ratingKey, })}` ); + } const next = playQueue[1]; - if (!next) + if (!next) { + if (room && isHost) socket?.emit("RES_SYNC_PLAYBACK_END"); return navigate( `/browse/${metadata.librarySectionID}?${queryBuilder({ mid: metadata.grandparentRatingKey, @@ -1713,6 +1836,7 @@ function Watch() { iid: metadata.ratingKey, })}` ); + } navigate(`/watch/${next.ratingKey}`); }} diff --git a/frontend/src/states/SyncSessionState.ts b/frontend/src/states/SyncSessionState.ts index 58a5c04..d580782 100644 --- a/frontend/src/states/SyncSessionState.ts +++ b/frontend/src/states/SyncSessionState.ts @@ -1,22 +1,19 @@ import { create } from "zustand"; import { EventEmitter } from "events"; -import { io } from "socket.io-client"; +import { io, Socket } from "socket.io-client"; import { getBackendURL, isDev } from "../backendURL"; +import { NavigateFunction } from "react-router-dom"; +import { useToast } from "../components/ToastManager"; export interface SyncSessionState { - socket: any; + socket: Socket | null; isHost: boolean; room: string | null; - connect: (room?: string) => Promise; + connect: (room?: string, navigate?: NavigateFunction) => Promise; disconnect: () => void; } -export interface SocketError { - type: string; - message: string; -} - export const SessionStateEmitter = new EventEmitter(); export const useSyncSessionState = create((set, get) => ({ @@ -24,8 +21,8 @@ export const useSyncSessionState = create((set, get) => ({ isHost: false, room: null, - connect: async (room) => { - return new Promise((resolve) => { + connect: async (room, navigate) => { + return new Promise((resolve) => { const socket = isDev ? io(getBackendURL(), { auth: { token: localStorage.getItem("accAccessToken") @@ -50,7 +47,7 @@ export const useSyncSessionState = create((set, get) => ({ console.log("Connected to server"); }); - (new Promise((resolve) => { + (new Promise((resolve) => { let resolved = false; socket.once("ready", (data) => { @@ -80,6 +77,7 @@ export const useSyncSessionState = create((set, get) => ({ } else { set({ socket }); resolve(true); + SocketManager(navigate); } }) @@ -97,4 +95,74 @@ export const useSyncSessionState = create((set, get) => ({ get().socket?.disconnect(); set({ socket: null, isHost: false, room: null }); } +})); + +function SocketManager(navigate: NavigateFunction | undefined) { + const { socket, isHost } = useSyncSessionState.getState(); + if (socket === null) return; + + + if (isHost) + { + socket.on("HOST_SYNC_GET_PLAYBACK", () => { + const { playBackState } = useSessionPlayBackCache.getState(); + console.log("Sending playback state", playBackState); + socket.emit("RES_SYNC_GET_PLAYBACK", playBackState); + }); + } + else + { + socket.on("RES_SYNC_SET_PLAYBACK", (user: PerPlexed.Sync.Member, data: PerPlexed.Sync.PlayBackState) => { + console.log("Playback state received", data); + navigate?.(`/watch/${data.key}?t=${data.time}`); + useToast.getState().addToast(user, "PlaySet", "Started Playback", 5000); + }); + + socket.on("RES_SYNC_RESYNC_PLAYBACK", (user: PerPlexed.Sync.Member, data: PerPlexed.Sync.PlayBackState) => { + console.log("Playback resync received", data); + SessionStateEmitter.emit("PLAYBACK_RESYNC", data); + }) + + socket.on("RES_SYNC_PLAYBACK_END", (user: PerPlexed.Sync.Member) => { + SessionStateEmitter.emit("PLAYBACK_END"); + }) + } + + socket.on("EVNT_SYNC_PAUSE", (user: PerPlexed.Sync.Member) => { + useToast.getState().addToast(user, "Pause", "Paused Playback", 5000); + SessionStateEmitter.emit("PLAYBACK_PAUSE"); + }) + + socket.on("EVNT_SYNC_RESUME", (user: PerPlexed.Sync.Member) => { + useToast.getState().addToast(user, "Play", "Resumed Playback", 5000); + SessionStateEmitter.emit("PLAYBACK_RESUME"); + }) + + socket.on("EVNT_SYNC_SEEK", (user: PerPlexed.Sync.Member, time: number) => { + SessionStateEmitter.emit("PLAYBACK_SEEK", time); + }) + + socket.on("EVNT_USER_JOIN", (user: PerPlexed.Sync.Member) => { + useToast.getState().addToast(user, "UserAdd", "Joined the session", 5000); + }); + + socket.on("EVNT_USER_LEAVE", (user: PerPlexed.Sync.Member) => { + useToast.getState().addToast(user, "UserRemove", "Left the session", 5000); + }); + + +} + +export interface SessionPlayBackCache { + playBackState: PerPlexed.Sync.PlayBackState | null; + + update: (data: PerPlexed.Sync.PlayBackState) => void; +} + +export const useSessionPlayBackCache = create((set, get) => ({ + playBackState: null, + + update: (data) => { + set({ playBackState: data }); + } })); \ No newline at end of file diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index 7883c99..4ab9137 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -15,9 +15,36 @@ declare namespace PerPlexed { interface Config { PLEX_SERVER: string; DEPLOYMENTID: string; - CONFIG: { - DISABLE_PROXY: boolean; - DISABLE_PERPLEXED_SYNC: boolean; + CONFIG: ConfigOptions + } + + interface ConfigOptions { + DISABLE_PROXY: boolean; + DISABLE_PERPLEXED_SYNC: boolean; + } + + namespace Sync { + interface SocketError { + type: string; + message: string; + } + + interface Ready { + room: string; + host: boolean; + } + + interface PlayBackState { + key?: string; + state: string; + time?: number; + } + + interface Member { + uid: string; + socket: string; + name: string; + avatar: string; } } } \ No newline at end of file