feat: allow rotating viewer (#220)

* feat: allow viewer mirroring
This commit is contained in:
Carlos Valente
2022-10-17 11:34:18 +02:00
committed by GitHub
parent 61cbce780d
commit 766ff40baa
14 changed files with 61 additions and 17 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime-ui", "name": "ontime-ui",
"version": "1.8.5", "version": "1.9.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@chakra-ui/react": "^2.3.2", "@chakra-ui/react": "^2.3.2",
@@ -0,0 +1,3 @@
import { atomWithStorage } from 'jotai/utils';
export const mirrorViewersAtom = atomWithStorage('ontime-viewers-mirrorViewers', false);
@@ -4,10 +4,13 @@ import { IconButton } from '@chakra-ui/button';
import { Image } from '@chakra-ui/react'; import { Image } from '@chakra-ui/react';
import { IoContract } from '@react-icons/all-files/io5/IoContract'; import { IoContract } from '@react-icons/all-files/io5/IoContract';
import { IoExpand } from '@react-icons/all-files/io5/IoExpand'; import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
import { IoSync } from '@react-icons/all-files/io5/IoSync';
import navlogo from 'assets/images/logos/LOGO-72.png'; import navlogo from 'assets/images/logos/LOGO-72.png';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useAtom } from 'jotai';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { mirrorViewersAtom } from '../../atoms/ViewerSettings';
import useFullscreen from '../../hooks/useFullscreen'; import useFullscreen from '../../hooks/useFullscreen';
import navigatorConstants from './navigatorConstants'; import navigatorConstants from './navigatorConstants';
@@ -26,6 +29,7 @@ export default function NavLogo(props) {
const { isHidden } = props; const { isHidden } = props;
const [showNav, setShowNav] = useState(false); const [showNav, setShowNav] = useState(false);
const { isFullScreen, toggleFullScreen } = useFullscreen(); const { isFullScreen, toggleFullScreen } = useFullscreen();
const [isMirrored, setMirrored] = useAtom(mirrorViewersAtom);
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
setShowNav((prev) => !prev); setShowNav((prev) => !prev);
@@ -57,6 +61,8 @@ export default function NavLogo(props) {
tabIndex: 0, tabIndex: 0,
}; };
console.log('debug', isMirrored)
return ( return (
<motion.div <motion.div
initial={{ opacity: showNav ? 0.5 : baseOpacity }} initial={{ opacity: showNav ? 0.5 : baseOpacity }}
@@ -75,6 +81,12 @@ export default function NavLogo(props) {
exit={{ opacity: 0, scaleX: 0 }} exit={{ opacity: 0, scaleX: 0 }}
className={style.actions} className={style.actions}
> >
<IconButton
aria-label='Mirror screen'
icon={<IoSync />}
onClick={() => setMirrored((prev) => !prev)}
{...navButtonStyle}
/>
<IconButton <IconButton
aria-label='Toggle Fullscreen' aria-label='Toggle Fullscreen'
icon={isFullScreen ? <IoContract /> : <IoExpand />} icon={isFullScreen ? <IoContract /> : <IoExpand />}
@@ -5,9 +5,11 @@ import Paginator from 'common/components/paginator/Paginator';
import TitleSide from 'common/components/title-side/TitleSide'; import TitleSide from 'common/components/title-side/TitleSide';
import { formatDisplay } from 'common/utils/dateConfig'; import { formatDisplay } from 'common/utils/dateConfig';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useAtom } from 'jotai';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { overrideStylesURL } from '../../../common/api/apiConstants'; import { overrideStylesURL } from '../../../common/api/apiConstants';
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { getEventsWithDelay } from '../../../common/utils/eventsManager'; import { getEventsWithDelay } from '../../../common/utils/eventsManager';
import { formatTime } from '../../../common/utils/time'; import { formatTime } from '../../../common/utils/time';
@@ -26,6 +28,7 @@ export default function Backstage(props) {
const [filteredEvents, setFilteredEvents] = useState(null); const [filteredEvents, setFilteredEvents] = useState(null);
const [pageNumber, setPageNumber] = useState(0); const [pageNumber, setPageNumber] = useState(0);
const [currentPage, setCurrentPage] = useState(0); const [currentPage, setCurrentPage] = useState(0);
const [isMirrored] = useAtom(mirrorViewersAtom);
// Set window title // Set window title
useEffect(() => { useEffect(() => {
@@ -58,7 +61,7 @@ export default function Backstage(props) {
const clock = formatTime(time.clock, formatOptions); const clock = formatTime(time.clock, formatOptions);
return ( return (
<div className='backstage'> <div className={`backstage ${isMirrored ? 'mirror' : ''}`}>
<NavLogo /> <NavLogo />
<div className='event-title'>{general.title}</div> <div className='event-title'>{general.title}</div>
+5 -5
View File
@@ -1,13 +1,12 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useAtom } from 'jotai';
import { overrideStylesURL } from '../../../common/api/apiConstants'; import { overrideStylesURL } from '../../../common/api/apiConstants';
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
import NavLogo from '../../../common/components/nav/NavLogo'; import NavLogo from '../../../common/components/nav/NavLogo';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { import { TimeManager, ViewSettings } from '../../../common/models/OntimeTypes';
TimeManager,
ViewSettings,
} from '../../../common/models/OntimeTypes';
import { OverridableOptions } from '../../../common/models/ViewTypes'; import { OverridableOptions } from '../../../common/models/ViewTypes';
import { formatTime } from '../../../common/utils/time'; import { formatTime } from '../../../common/utils/time';
@@ -27,6 +26,7 @@ export default function Clock(props: ClockProps) {
const { time, viewSettings } = props; const { time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [isMirrored] = useAtom(mirrorViewersAtom);
useEffect(() => { useEffect(() => {
document.title = 'ontime - Clock'; document.title = 'ontime - Clock';
@@ -127,7 +127,7 @@ export default function Clock(props: ClockProps) {
return ( return (
<div <div
className='clock-view' className={`clock-view ${isMirrored ? 'mirror' : ''}`}
style={{ style={{
backgroundColor: userOptions.keyColour, backgroundColor: userOptions.keyColour,
color: userOptions.textColour, color: userOptions.textColour,
@@ -1,8 +1,10 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom'; import { Link, useSearchParams } from 'react-router-dom';
import { useAtom } from 'jotai';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { overrideStylesURL } from '../../../common/api/apiConstants'; import { overrideStylesURL } from '../../../common/api/apiConstants';
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
import NavLogo from '../../../common/components/nav/NavLogo'; import NavLogo from '../../../common/components/nav/NavLogo';
import Empty from '../../../common/components/state/Empty'; import Empty from '../../../common/components/state/Empty';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
@@ -23,6 +25,7 @@ export default function Countdown(props) {
const { backstageEvents, time, selectedId, viewSettings } = props; const { backstageEvents, time, selectedId, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [isMirrored] = useAtom(mirrorViewersAtom);
const [follow, setFollow] = useState(null); const [follow, setFollow] = useState(null);
const [runningTimer, setRunningTimer] = useState(0); const [runningTimer, setRunningTimer] = useState(0);
@@ -90,7 +93,7 @@ export default function Countdown(props) {
: formatTime(follow.timeEnd + delay, formatOptions); : formatTime(follow.timeEnd + delay, formatOptions);
return ( return (
<div className='countdown'> <div className={`countdown ${isMirrored ? 'mirror' : ''}`}>
<NavLogo /> <NavLogo />
{follow === null ? ( {follow === null ? (
<div className='event-select'> <div className='event-select'>
@@ -139,7 +142,7 @@ export default function Countdown(props) {
> >
{formatDisplay( {formatDisplay(
isSelected ? runningTimer : runningTimer + millisToSeconds(delay), isSelected ? runningTimer : runningTimer + millisToSeconds(delay),
isSelected || time.waiting isSelected || time.waiting,
)} )}
</span> </span>
<div className='title'>{follow?.title || 'Untitled Event'}</div> <div className='title'>{follow?.title || 'Untitled Event'}</div>
@@ -1,7 +1,9 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useAtom } from 'jotai';
import { overrideStylesURL } from '../../../common/api/apiConstants'; import { overrideStylesURL } from '../../../common/api/apiConstants';
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
import NavLogo from '../../../common/components/nav/NavLogo'; import NavLogo from '../../../common/components/nav/NavLogo';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { import {
@@ -24,6 +26,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
const { pres, time, viewSettings } = props; const { pres, time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [isMirrored] = useAtom(mirrorViewersAtom);
useEffect(() => { useEffect(() => {
document.title = 'ontime - Minimal Timer'; document.title = 'ontime - Minimal Timer';
@@ -131,9 +134,11 @@ export default function MinimalTimer(props: MinimalTimerProps) {
const clean = timer.replace('/:/g', ''); const clean = timer.replace('/:/g', '');
const showFinished = time.isNegative && !userOptions?.hideOvertime; const showFinished = time.isNegative && !userOptions?.hideOvertime;
const baseClasses = `minimal-timer ${isMirrored ? 'mirror' : ''}`;
return ( return (
<div <div
className={showFinished ? 'minimal-timer minimal-timer--finished' : 'minimal-timer'} className={showFinished ? `${baseClasses} minimal-timer--finished` : baseClasses}
style={{ style={{
backgroundColor: userOptions.keyColour, backgroundColor: userOptions.keyColour,
color: userOptions.textColour, color: userOptions.textColour,
@@ -12,6 +12,8 @@ import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet
import { formatTime } from '../../../common/utils/time'; import { formatTime } from '../../../common/utils/time';
import './Pip.scss'; import './Pip.scss';
import { useAtom } from 'jotai';
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
const formatOptions = { const formatOptions = {
showSeconds: true, showSeconds: true,
@@ -25,6 +27,7 @@ export default function Pip(props) {
const [filteredEvents, setFilteredEvents] = useState(null); const [filteredEvents, setFilteredEvents] = useState(null);
const [pageNumber, setPageNumber] = useState(0); const [pageNumber, setPageNumber] = useState(0);
const [currentPage, setCurrentPage] = useState(0); const [currentPage, setCurrentPage] = useState(0);
const [isMirrored] = useAtom(mirrorViewersAtom);
// Set window title // Set window title
useEffect(() => { useEffect(() => {
@@ -65,7 +68,7 @@ export default function Pip(props) {
const clock = formatTime(time.clock, formatOptions); const clock = formatTime(time.clock, formatOptions);
return ( return (
<div className='pip'> <div className={`pip ${isMirrored ? 'mirror' : ''}`}>
<NavLogo /> <NavLogo />
<div className='event-title'>{general.title}</div> <div className='event-title'>{general.title}</div>
@@ -4,9 +4,11 @@ import NavLogo from 'common/components/nav/NavLogo';
import Paginator from 'common/components/paginator/Paginator'; import Paginator from 'common/components/paginator/Paginator';
import TitleSide from 'common/components/title-side/TitleSide'; import TitleSide from 'common/components/title-side/TitleSide';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useAtom } from 'jotai';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { overrideStylesURL } from '../../../common/api/apiConstants'; import { overrideStylesURL } from '../../../common/api/apiConstants';
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { formatTime } from '../../../common/utils/time'; import { formatTime } from '../../../common/utils/time';
import { titleVariants } from '../common/animation'; import { titleVariants } from '../common/animation';
@@ -23,6 +25,7 @@ export default function Public(props) {
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const [pageNumber, setPageNumber] = useState(0); const [pageNumber, setPageNumber] = useState(0);
const [currentPage, setCurrentPage] = useState(0); const [currentPage, setCurrentPage] = useState(0);
const [isMirrored] = useAtom(mirrorViewersAtom);
// Set window title // Set window title
useEffect(() => { useEffect(() => {
@@ -40,7 +43,7 @@ export default function Public(props) {
const clock = formatTime(time.clock, formatOptions); const clock = formatTime(time.clock, formatOptions);
return ( return (
<div className='public-screen'> <div className={`public-screen ${isMirrored ? 'mirror' : ''}`}>
<NavLogo /> <NavLogo />
<div className='event-title'>{general.title}</div> <div className='event-title'>{general.title}</div>
@@ -1,7 +1,9 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useAtom } from 'jotai';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { overrideStylesURL } from '../../../common/api/apiConstants'; import { overrideStylesURL } from '../../../common/api/apiConstants';
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
import NavLogo from '../../../common/components/nav/NavLogo'; import NavLogo from '../../../common/components/nav/NavLogo';
import useFitText from '../../../common/hooks/useFitText'; import useFitText from '../../../common/hooks/useFitText';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
@@ -28,6 +30,7 @@ export default function StudioClock(props) {
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ maxFontSize: 500 }); const { fontSize: titleFontSize, ref: titleRef } = useFitText({ maxFontSize: 500 });
const [schedule, setSchedule] = useState([]); const [schedule, setSchedule] = useState([]);
const [isMirrored] = useAtom(mirrorViewersAtom);
const activeIndicators = [...Array(12).keys()]; const activeIndicators = [...Array(12).keys()];
const secondsIndicators = [...Array(60).keys()]; const secondsIndicators = [...Array(60).keys()];
@@ -48,13 +51,13 @@ export default function StudioClock(props) {
showEnd: false, showEnd: false,
}); });
setSchedule(formatted); setSchedule(formatted);
}, [backstageEvents, nextId, selectedId] ); }, [backstageEvents, nextId, selectedId]);
const clock = formatTime(time.clock, formatOptions); const clock = formatTime(time.clock, formatOptions);
const [, , secondsNow] = stringFromMillis(time.clock).split(':'); const [, , secondsNow] = stringFromMillis(time.clock).split(':');
return ( return (
<div className='studio-clock'> <div className={`studio-clock ${isMirrored ? 'mirror' : ''}`}>
<NavLogo /> <NavLogo />
<div className='clock-container'> <div className='clock-container'>
<div className='studio-timer'>{clock}</div> <div className='studio-timer'>{clock}</div>
@@ -65,7 +68,8 @@ export default function StudioClock(props) {
> >
{title.titleNext} {title.titleNext}
</div> </div>
<div className={time.isNegative ? 'next-countdown' : 'next-countdown next-countdown--overtime'}> <div
className={time.isNegative ? 'next-countdown' : 'next-countdown next-countdown--overtime'}>
{selectedId != null && formatDisplay(time.running)} {selectedId != null && formatDisplay(time.running)}
</div> </div>
<div className='clock-indicators'> <div className='clock-indicators'>
+5 -1
View File
@@ -5,9 +5,11 @@ import MyProgressBar from 'common/components/myProgressBar/MyProgressBar';
import NavLogo from 'common/components/nav/NavLogo'; import NavLogo from 'common/components/nav/NavLogo';
import TitleCard from 'common/components/title-card/TitleCard'; import TitleCard from 'common/components/title-card/TitleCard';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useAtom } from 'jotai';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { overrideStylesURL } from '../../../common/api/apiConstants'; import { overrideStylesURL } from '../../../common/api/apiConstants';
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { formatTime } from '../../../common/utils/time'; import { formatTime } from '../../../common/utils/time';
@@ -23,6 +25,7 @@ export default function Timer(props) {
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const [elapsed, setElapsed] = useState(true); const [elapsed, setElapsed] = useState(true);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [isMirrored] = useAtom(mirrorViewersAtom);
useEffect(() => { useEffect(() => {
document.title = 'ontime - Timer'; document.title = 'ontime - Timer';
@@ -64,9 +67,10 @@ export default function Timer(props) {
y: 500, y: 500,
}, },
}; };
const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''}`;
return ( return (
<div className={time.finished ? 'stage-timer stage-timer--finished' : 'stage-timer'}> <div className={time.finished ? `${baseClasses} stage-timer--finished` : baseClasses}>
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}> <div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
<div className='message'>{pres.text}</div> <div className='message'>{pres.text}</div>
</div> </div>
+3
View File
@@ -0,0 +1,3 @@
.mirror {
transform: rotate(180deg);
}
+1
View File
@@ -1,4 +1,5 @@
@use 'main' as *; @use 'main' as *;
@use 'viewerCommon' as *;
// General styling // General styling
$accent-color: $ontime-pink; // --accent-color-override $accent-color: $ontime-pink; // --accent-color-override
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "1.8.5", "version": "1.9.0",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",