diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx
index e929b66a5..6ebce9acb 100644
--- a/apps/client/src/App.tsx
+++ b/apps/client/src/App.tsx
@@ -4,16 +4,15 @@ import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
+import IdentifyOverlay from './common/components/identify-overlay/IdentifyOverlay';
import { AppContextProvider } from './common/context/AppContext';
import { ontimeQueryClient } from './common/queryClient';
-import { socketClientName } from './common/stores/connectionName';
import { connectSocket } from './common/utils/socket';
import theme from './theme/theme';
import { TranslationProvider } from './translation/TranslationProvider';
import AppRouter from './AppRouter';
-const preferredClientName = socketClientName.getState().name;
-connectSocket(preferredClientName);
+connectSocket();
function App() {
return (
@@ -24,11 +23,15 @@ function App() {
+
+
+
diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx
index 2f34d2acf..ba146279a 100644
--- a/apps/client/src/AppRouter.tsx
+++ b/apps/client/src/AppRouter.tsx
@@ -1,6 +1,7 @@
import { lazy, Suspense } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
+import { useClientPath } from './common/hooks/useClientPath';
import Log from './features/log/Log';
import withPreset from './features/PresetWrapper';
import withData from './features/viewers/ViewWrapper';
@@ -34,6 +35,9 @@ const TimerControl = lazy(() => import('./features/control/playback/TimerControl
const MessageControl = lazy(() => import('./features/control/message/MessageControlExport'));
export default function AppRouter() {
+ // handle client path changes
+ useClientPath();
+
return (
diff --git a/apps/client/src/common/api/constants.ts b/apps/client/src/common/api/constants.ts
index ea06255b5..936317936 100644
--- a/apps/client/src/common/api/constants.ts
+++ b/apps/client/src/common/api/constants.ts
@@ -11,6 +11,7 @@ export const RUNTIME = ['runtimeStore'];
export const SHEET_STATE = ['sheetState'];
export const URL_PRESETS = ['urlpresets'];
export const VIEW_SETTINGS = ['viewSettings'];
+export const CLIENT_LIST = ['clientList'];
// resolve location
const location = window.location;
diff --git a/apps/client/src/common/components/client-modal/ClientModal.module.scss b/apps/client/src/common/components/client-modal/ClientModal.module.scss
new file mode 100644
index 000000000..2c291157e
--- /dev/null
+++ b/apps/client/src/common/components/client-modal/ClientModal.module.scss
@@ -0,0 +1,6 @@
+.buttonSection {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: $element-spacing;
+ margin-top: -$element-spacing;
+}
diff --git a/apps/client/src/common/components/client-modal/RedirectClientModal.tsx b/apps/client/src/common/components/client-modal/RedirectClientModal.tsx
new file mode 100644
index 000000000..163e481ba
--- /dev/null
+++ b/apps/client/src/common/components/client-modal/RedirectClientModal.tsx
@@ -0,0 +1,69 @@
+import { useState } from 'react';
+import {
+ Button,
+ Input,
+ InputGroup,
+ InputLeftAddon,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+} from '@chakra-ui/react';
+
+import { setClientRemote } from '../../hooks/useSocket';
+
+import style from './ClientModal.module.scss';
+
+interface RedirectClientModalProps {
+ id: string;
+ name?: string;
+ path?: string;
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export function RedirectClientModal(props: RedirectClientModalProps) {
+ const { id, isOpen, name = '', path: currentPath = '', onClose } = props;
+ const [path, setPath] = useState(currentPath);
+
+ const { setRedirect } = setClientRemote;
+
+ const handleRedirect = () => {
+ if (path !== currentPath && path !== '') {
+ setRedirect({ target: id, redirect: path });
+ }
+ onClose();
+ };
+
+ const host = `${window.location.origin}/`;
+ const canSubmit = path !== currentPath && path !== '';
+
+ return (
+
+
+
+ Redirect: {name}
+
+
+
+ {host}
+ setPath(event.target.value)} />
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/client/src/common/components/client-modal/RenameClientModal.tsx b/apps/client/src/common/components/client-modal/RenameClientModal.tsx
new file mode 100644
index 000000000..a66991278
--- /dev/null
+++ b/apps/client/src/common/components/client-modal/RenameClientModal.tsx
@@ -0,0 +1,68 @@
+import { useState } from 'react';
+import {
+ Button,
+ Input,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+} from '@chakra-ui/react';
+
+import { setClientRemote } from '../../hooks/useSocket';
+
+import style from './ClientModal.module.scss';
+
+interface RenameClientModalProps {
+ id: string;
+ name?: string;
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export function RenameClientModal(props: RenameClientModalProps) {
+ const { id, name: currentName = '', isOpen, onClose } = props;
+ const [name, setName] = useState(currentName);
+
+ const { setClientName } = setClientRemote;
+
+ const handleRename = () => {
+ if (name !== currentName && name !== '') {
+ setClientName({ target: id, rename: name });
+ }
+ onClose();
+ };
+
+ const canSubmit = name !== currentName && name !== '';
+
+ return (
+
+
+
+ Rename: {currentName}
+
+
+ setName(event.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/client/src/common/components/identify-overlay/IdentifyOverlay.module.scss b/apps/client/src/common/components/identify-overlay/IdentifyOverlay.module.scss
new file mode 100644
index 000000000..ac65bc290
--- /dev/null
+++ b/apps/client/src/common/components/identify-overlay/IdentifyOverlay.module.scss
@@ -0,0 +1,34 @@
+.overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ margin: 0;
+ width: 100vw;
+ height: 100vh;
+ padding: 0;
+ box-sizing: border-box;
+
+ border: 10px solid $ui-white;
+ background-color: $ui-black;
+
+ color: $ui-white;
+ display: grid;
+ place-content: center;
+ text-align: center;
+ z-index: 100;
+ cursor: pointer;
+}
+
+.name {
+ font-weight: 600;
+ margin-top: -10vh;
+ line-height: 1em;
+ font-size: 12vw;
+ text-transform: uppercase;
+}
+
+.message {
+ color: $label-gray;
+ line-height: 2em;
+ font-size: 3vw;
+}
diff --git a/apps/client/src/common/components/identify-overlay/IdentifyOverlay.tsx b/apps/client/src/common/components/identify-overlay/IdentifyOverlay.tsx
new file mode 100644
index 000000000..2f1d32c36
--- /dev/null
+++ b/apps/client/src/common/components/identify-overlay/IdentifyOverlay.tsx
@@ -0,0 +1,64 @@
+import { useCallback, useEffect, useRef } from 'react';
+import { createPortal } from 'react-dom';
+import { MILLIS_PER_MINUTE } from 'ontime-utils';
+
+import { setClientRemote } from '../../hooks/useSocket';
+import { useClientStore } from '../../stores/clientStore';
+
+import style from './IdentifyOverlay.module.scss';
+
+export default function IdentifyOverlay() {
+ const clients = useClientStore((store) => store.clients);
+ const id = useClientStore((store) => store.id);
+ const showOverlay = clients[id]?.identify;
+
+ if (!showOverlay) {
+ return null;
+ }
+
+ const portalRoot = document.getElementById('identify-portal');
+
+ if (!portalRoot) {
+ return null;
+ }
+ return createPortal(, portalRoot);
+}
+
+function Overlay() {
+ const clients = useClientStore((store) => store.clients);
+ const id = useClientStore((store) => store.id);
+ const name = useClientStore((store) => store.name);
+
+ const timerRef = useRef(null);
+
+ const { setIdentify } = setClientRemote;
+ const showOverlay = clients[id]?.identify;
+
+ const handleClose = useCallback(() => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
+
+ setIdentify({ target: id, identify: false });
+ }, [id, setIdentify]);
+
+ // start a timer that will close the overlay after some time
+ useEffect(() => {
+ if (showOverlay) {
+ timerRef.current = setTimeout(handleClose, MILLIS_PER_MINUTE);
+ }
+ return () => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
+ };
+ }, [showOverlay, id, setIdentify, handleClose]);
+
+ console.log('here2');
+ return (
+
+
{name}
+
Click to close
+
+ );
+}
diff --git a/apps/client/src/common/components/navigation-menu/NavigationMenu.tsx b/apps/client/src/common/components/navigation-menu/NavigationMenu.tsx
index 2e2a4ee19..59ba42937 100644
--- a/apps/client/src/common/components/navigation-menu/NavigationMenu.tsx
+++ b/apps/client/src/common/components/navigation-menu/NavigationMenu.tsx
@@ -19,10 +19,10 @@ import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
import { navigatorConstants } from '../../../viewerConfig';
import useClickOutside from '../../hooks/useClickOutside';
+import { useClientStore } from '../../stores/clientStore';
import { useViewOptionsStore } from '../../stores/viewOptions';
import { isKeyEnter } from '../../utils/keyEvent';
-
-import RenameClientModal from './rename-client-modal/RenameClientModal';
+import { RenameClientModal } from '../client-modal/RenameClientModal';
import style from './NavigationMenu.module.scss';
@@ -34,8 +34,10 @@ interface NavigationMenuProps {
function NavigationMenu(props: NavigationMenuProps) {
const { isOpen, onClose } = props;
- const { isOpen: isRenameOpen, onOpen: onRenameOpen, onClose: onRenameClose } = useDisclosure();
+ const id = useClientStore((store) => store.id);
+ const name = useClientStore((store) => store.name);
+ const { isOpen: isOpenRename, onOpen: onRenameOpen, onClose: onCloseRename } = useDisclosure();
const { fullscreen, toggle } = useFullscreen();
const { toggleMirror } = useViewOptionsStore();
@@ -45,7 +47,7 @@ function NavigationMenu(props: NavigationMenuProps) {
return createPortal(