D?`ML;209>ZmCZsJjH!>Ms&C=~k$lxp>1
z=DT9AVZI+b7V%w3!Eh!{K=CL(sBsU88TMg3T+zd^Hz_E}4n%y0!==SWR~VOL2cLeL
z$4;JLa~3egPGnO?4xUSY(DR6FSBJx5p)ofpcPQv%kro
z?yPQUh6F6|Dp)(@s*$!O;=6xpp@J(S7?|G6K=F=5?&*{>U2eSjekk_J*7rlk45Mn8
z>#;Kuw+}o>5PJ>9AI5k+qGTWEM>%4vlR%dz^z6vPLPP?E&~eR@)h1Obd7T(7&J!0%
zre_7mM%UPnFs-$^1yQ#Wqg0hB;ar%EyLdO}2+tciH|j7i$}t()n8)OQt_3{z;@qPK
znNzM9qfv#LWLj|`**W~%fRL#YMSMkr1jrvVvY;pHOI
zY*#0K$|C2{Veo;p0pBWgjN6mZ0WBBHqsD`_iJ-^|jfw6slp$ciRalU^xq6ZzbHP0V
z9BgV{Iuf~zL@qm#!}kl!dOkA;DiX&QzuUkfsOZNf5bNLJP^hiNJ#U`7r7e*&9tYmh
zkh>)mYH=fz$WsI&rhdS9$cFDN$eBa&<^oN;vxqlU8XNFVGlx127bqGzC9)w)6i%cj
zNr{04E|*#V#yyEFGHmxS*1IaS?vNAlUYg>)D$04K00CE{)p$sKPsmMN!CjhRtyz^=
zH15IRa4}*#0BpxT(2jwD$GzPBK@JE~!K=brqMk^&~eQ)2I9`MsD@3EFbfy6jT&$W}&qqy$&LsYi>=
zxY4A0I^+dKTB4TxU>nh{-ECG1z0nYt;19jKV8^g-fk>gyV*l*TP}9be;1j1A>i
z9;~Qx`Gy{u3OtN0Q&}ZBEX~=4@vx(e5`}gI=R+Ar4`!T{DdO=8Z6cDq{W!rNxgpQ`OUdmB*6uA>F$RuKkpz|!DY^J*DVE(IoE>s7zV
zRLeA9TVqbm$ty)eDWWX{uW}41L6hL?ab4IUcMY2Wv=h$1lmZ9h9
zkHum1HCU!SIfGu8Oi&{mB1W%wl<1r>l<1@%Q#ErOs+w2H2a+lSYm$fhw8<-~$I4I{
zW+C#OaP={kC0HV{{c_r`{0e
z8K8EuUZi~-;AZsX1x@XmBx={7#}+RQo{Neq!%({#45H`Z@DfX8phmL9;`2y#&i{$Y
zROae8l*$lqsOSS%mgD*fC752fQdFhk$U1~yhf-AeJO?RDYUB^g9@t@x&|;nB(rt<%
z4B3u_m3?xcPX^L7+F;alx1)j++V*y6yfx5xn=P$3x+i*!sxL&9(#h2`GX&3}T}Ks~
z_EhpS(`gN)VWwIdw1H=Nw6;!NJ!%|@ehkxX51-i~yNzz!EVP^{)qE!IrO-{s{RZT-
zwtzCs9PL9GVIG)dO^3Vs==tsIGP?U}V~^JMDIBBn9&CUk5G7e!qeuVJ;U5^#o-ADn
zK0azp7U3PHL+J=@1Lb>D&l?UuTEFxPleUJEz1;uJ9Pk5r)dfvJu!Fb}N1L~{&(~Lr
z$ED~{F6ftjc+9?{SDUgV4yXQ?juE0Gbg+I&EixO|=(SBpo*D*y^BjRf=$(0JrciM^
zOQ=Bcjv}P2P2MgZ2!<#(}W%FKhu1bWx55pC7we^U7zWIO+@uD4-;+l
z_70_V7=q{>J=ci#8KFf1(ookI&nUXD{{Xn!v
ziE1`5T~1Tn0JrmiM>9z+Pn!B}8@-*1$j!8j{nTmwpKLmXcS4bm?(sz;zCBUsJ6*7)
zdQlWsO3n!-?P0?es&sYuA{BiV5nqR^GUP&lY^WYof!;i!IddYp3kNNqiJ%zQt3sS7
z>%QeFG8flSFBkRb3njnN{&8c<)+kqa;)Hv*i=VECeZig0X7*#6coulg3H=
zMM`Cn9w`@ursYw!yd3NiJsc&}E(UxU9@P_z2Q424*CCHuvaA&+$%~OZZq`bj?88VI
zzu}!2);K2f8vEdvdAbK_7*zIv5{NU8DS6F}6h3OG_K-eSFD&fyz!y4NtyjsK0ow|W
z`4l;mY*DE&vAIAdY>wD{mM|GAhiQ*Kp?NLY9;HPzyj4EfQXSd4NVe3It&9Jw{R#hW
zr7_r=7&Msh^zI(yARJ~mE#JoQ%UXgrEuW1SUZfx`P8`(da;eRva6k@6qsNG2ET)*=72q)tcP(KwscU-H_aI|to=?0b#i5(-4n*~EYD65xKdvEQATOZHJQ?v
ze33fK^f@IrX9q1%KT>+uS)aNK%d5y6RD0+!rtU(Q*l_&OT#ImlQLO^1o|a+xgAr}|
z`Vym+VgMdEh7kBRQ*`(&_JCEAJbqr8%|6qZawzeejg+Q}UQ^i{+JX7x6%R1wTdpV!
zS(w47jTUBJ27H5d&BVRL2Ua2WxV#otTvTOZ5qlq(jP3+31M-Y}Zy@EBP5-PO1={r5
zX9&vn~hhsWvgLK<1#zU|wQ8I0i0WJ&4s;U0tp+TK;iXdQr>bK%TQt44~1is{N3(
zfH=k+AC4DYN0fG~U4@k8u(%Ysde-3m^`rjF`1&`8>^=d{vv5Lv4l#*>H
zQZEr6JQ{o7oO2_pm*(30s_kX0IN9*xP`+KMDMBfw(v)VwBn~j@+IC0wM9BbTh}dl@
ztiWr%W(O6MVn5U{d|}0j8E#h^1vFS7VOhQ$S8BDKZ4;1skuu`tNAHr?D~_;=WtHPw
z*3_4*c(S45$;!Mkwb&X&lOc$cV#{!wheG`!A41AdVPgjYAZuB6%!)D}SCM6{xkiTu
zoRejZ6*thxuq$RhsnF}rPHar
zAB?W!L|V>JD^3@e=2v<-&IA=w75YK8&>-bduSu4glB&I1>k;jq_@A)fjQi@eUQEuc
zm2c~Q)9&F0HXCpc0at3iSwNKouC4D(T;W{J#8vi`448#HSB`wtEPyC8Yoe5T46o9I
zVMj_DP&QERGKIYZ7ErX%u7UzYE6E92Xp*$hVKonCa|x~#nA9miSYPcoat<5AbQ?|B
z>Q@VMJnB*lpgd`@ET`4Lrml-EnkO=zp*A|0Bcnvtw5?|AlEF4&mz!?^wT`0
zjY@hpC!jppv*~X#(yJQ~tWrvEazw6%0W3@7L1+QHO^&Ix%&1F>%f${_VVVx?=sFfQ
zmlC!8ClQ-FuI$HzEacu*YaiTGn%@HCH5`Jz1ke|2o(=X8lUIhthC~M!|Zpnh<3v5ZK=AVj%%gt
zYU94wNr{*_H|SzH)B9dIJ-|Ug%4zl@ldC^tD|(+Rl`SQhxE_;VxzdpXS%b5fHgq8vrzlaAC8g{)mwIX|ZX88iv2};wph3Lf
zZD6Bhi$92U&fXkR%d%3K`}#NwBfdUkwYe0V%JQ*Z7Q>YoiG9?YBA-s6w`m!TK54xT
zZ%Ea88#9=L=q3A1mEDCjxs^$h|8~-gItD9@vN3Qf;5nGyBMp#e8S=Iq
z`>C=q)pAbW0c>q0vYbE3fqqy=<~E`fyGTpJS%bDhBiP6}b*b_+lE1r!z8`heMC-<{y*c?nk^%{R56e!PQR&9UHG@l)l8g+Ex)MavkM
z#e)q-GT-@Gh}}>T)A}6eb2bRf6_nG|o4CRf?8DTS
zGp%UKIWKWYm|PeO?G?a~z#FY3xGx;>^{QPy;a(s1wOW$fSOx>y+Qbyw<%?h(Yj}TIjY@-=Dg$5L?NBQ_L|}H4#HG$8g_&izg^pB*sii7ju6L``9HPh@U
z2_y|IWekCF0}bRCKzKU+^x|m99$TYvvF$B%*lkjoN{fRnV)W3Ti$gnIA=_0U*G@E%
ztS4%HXFM*0*rMel8_{qckIRh5Wya$&N=F)x%czxcJT610*LYk;ZHXR_%V_<}+>!jh
zvZ9CVQW=lSAVN1Dm(kwo@wiMCtQiOF92zm>aT#U9j>l!j<1*uMnen)c4E8P3^D`co
z8IQ~8ZBpZL8TA60@wkj;hw-?K`eM;|Tqd!B`sBrB((s*jF;*wVWy-C{#i%1QMTxx<
z!$fEj-=EPDnr1UXV^?n)+8yy(aT;I5jML!VHGDk-0$4}Ym*IWUqrOPLj@4klJwA*l
zsThRx0&nNQy9*eCKs19fg*3zzTCj7AZ_esP_yIwPFoF^0Td^3Y$Je@y`#)8`^RX$1
zktip2{4llp?LaxdgTcVA-gJky*>Aiu1F!F?Zh`-;*{)t1Q>)*wbU4x50SX{s#*TNe
zx3X}|-a2GO7a74zhXKazWK=M$
zubr~o3P9!RZ#uy<=2Rn6qhPIq`W;f{d!TxWfc286K6Rp@p}sSwUfQHUgx9taaGda)
zw(+>}wi#PvEO_vHxfaSIW0H=Qm6=~v!&`t9fwOR~-y)C!l=b(WbO~SKgBL(4qBod1
zymkh)_|~BF6Zf5uuFpcmea=7$>a?NGIcCXz{9wrWG7C@A_b;~MZ`{DZFaOlzXvHy?
zDQ=$0@ID^k!Ecbznqj2U%9Wcr{RXcp)T&Ngn}pm<)eN7~a7e#Bs@W{%i;eVkqGr{c
ziJA-Wl!YkKmXp_+ex}h>%XsW?ZzDQEc@1NR1-1IUMRWw~FIqKuTV}Y8+TZSp7(cAI
z(}jp#TL|3Nk;wI_TrJ90B+6p1;Pf(e%9x|hh1DaByB{9}=((gv@>gLj!I^#h|l#3~b5eT|rLMvl#U@Cr#+B%F#WRhUCuJ+F6W;zZqE#5-s@W6
zdOxcyt1YW5Yj@Ux?A9Dt&Lg>Px%atC+&8*^p0_&hh$qj}<2jZ;BmbA)v%HV`Ciq?~
zSXS_1VPoMV{(S$F{*Q`oEPAfkRa{rRwfO7B2a4Y)=_$=G?I=B3c5d1I<
z-TrLm@k><3WIRoOK5Lv|<_)}G^nNpM69<0nUe~ds{4q0+H&7Y;ZU8@NQf%~@dF*&I
zer@J6Fn8WH^O@qYWt;hIDYliH`5c*Jn_=d?;<9bQo(aQ|iKp(f-D2hqDYKtu=FwmK
zVl!_?`4wi~f&6u5K25^*FPZstDY5_9%x6f%{%bRzDOHYgGY^_L&NTBmvdwXgnFoCw
zAK>@2y77Bc?f7-&PRNk}e$n|lz(x2Ks_RkGh2Lu4fb%){wdPK6VLM7Xkn6^`m+|{j
zIKuedu137?lK1rBUe=q6U+>~|-n9;CH=ec%ziicmUvlPICsICRGhhd5&Q;gf;Cegi
z^X`RcwF}44D?>{z!851fDa0dy-vL{J=LC=stMVrNhFBQiPsT5EtLqlN#HL3!qPzir
zThw1Y@Ur?q42fSCp1cB>tjFC$exK~@?#}j}&W^xU*98`>xxTY&?S{adb)CKKT^*g>
z^?`2%!i|kH1B-gDTh}?Yr5=~o1-jR)THUiL(A~MIvwL%AM}1&*PtV3#4GmkiY^mSG
zikmt&cXrjU*sy-?npJCh+Sg5O2`t>uwSh-!N$T0O`o?fz&c^OQV@qIKc-FM$S&fZo
ze%iD|H9lcve1~-5gt)bJcXn=R@9qq&+|V7^*x9{lLs$E{z_pt?>vLRfC<6NHJG)n`
z(S?DH>pI&vb)vMZ=h_uLs8`=OE!;G{c}C03)Bc}ez<^@TPsa{J88MJx+Y__zK%J59
z*X&FNo&y%`R_xrO*f|V_orV`zCbRQ2lni~Eg~R`4f`76Y`1I4LO$mTbOt^NH3&=`L
zFZOYx5(=Bt)TbZ_$hAsP)MKURE2(uYJsH=lx+@?!l56rA6hQ6jzJQvzU6|6>nnHok
zqo5cvktyeG1h8H1KvTng) {
@@ -30,9 +32,10 @@ export default function NavLogo() {
};
}, [handleKeyPress]);
+ const baseOpacity = (isHidden) ? 0 : 0.5
return (
@@ -51,12 +54,12 @@ export default function NavLogo() {
className={showNav ? style.nav : style.navHidden}
>
}
+ onKeyDownCapture={() => }
>
- Speaker
+ Presenter
PIP
+ }
+ >
+ Studio Clock
+
)}
);
}
+
+NavLogo.propTypes = {
+ isHidden: PropTypes.bool,
+}
diff --git a/client/src/common/components/nav/NavLogo.module.css b/client/src/common/components/nav/NavLogo.module.scss
similarity index 87%
rename from client/src/common/components/nav/NavLogo.module.css
rename to client/src/common/components/nav/NavLogo.module.scss
index ad75df5b4..3f594bfe9 100644
--- a/client/src/common/components/nav/NavLogo.module.css
+++ b/client/src/common/components/nav/NavLogo.module.scss
@@ -1,6 +1,9 @@
+$nav-color: #fff;
+
.navContainer {
position: absolute;
right: 2vw;
+ top: 1vw;
display: flex;
flex-direction: column;
align-items: flex-end;
@@ -23,4 +26,5 @@
background-color: rgba(0, 0, 0, 0.7);
padding: 0.5vh 1vw;
border-radius: 4px;
+ color: $nav-color;
}
diff --git a/client/src/common/utils/__tests__/eventsManager.test.js b/client/src/common/utils/__tests__/eventsManager.test.js
new file mode 100644
index 000000000..68e26c91c
--- /dev/null
+++ b/client/src/common/utils/__tests__/eventsManager.test.js
@@ -0,0 +1,410 @@
+import {formatEventList, getEventsWithDelay, trimEventlist} from "../eventsManager";
+
+test('getEventsWithDelay function', () => {
+
+ const testData = [
+ {
+ "title": "Welcome to Ontime",
+ "timeStart": 28800000,
+ "timeEnd": 30600000,
+ "type": "event",
+ "id": "5946"
+ },
+ {
+ "duration": 60000,
+ "type": "delay",
+ "id": "24240"
+ },
+ {
+ "title": "Unless recalled by the OSC address",
+ "timeStart": 34920000,
+ "timeEnd": 35520000,
+ "type": "event",
+ "id": "8ee5"
+ },
+ {
+ "title": "Use simpler times to create a timer",
+ "timeStart": 120000,
+ "timeEnd": 720000,
+ "type": "event",
+ "id": "8222"
+ },
+ {
+ "duration": 900000,
+ "type": "delay",
+ "revision": 0,
+ "id": "a386"
+ },
+ {
+ "title": "Add delay blocks to affect all events",
+ "timeStart": 37320000,
+ "timeEnd": 38520000,
+ "type": "event",
+ "id": "6dce"
+ },
+ {
+ "title": "Add and remove events with [+] and [-]",
+ "timeStart": 38520000,
+ "timeEnd": 45120000,
+ "type": "event",
+ "id": "2651"
+ },
+ {
+ "type": "block",
+ "id": "e6a1"
+ },
+ {
+ "title": "And control whether they are public",
+ "timeStart": 46800000,
+ "timeEnd": 57600000,
+ "type": "event",
+ "id": "1358"
+ }
+ ];
+
+ const expected = [
+ {
+ "title": "Welcome to Ontime",
+ "timeStart": 28800000,
+ "timeEnd": 30600000,
+ "type": "event",
+ "id": "5946"
+ },
+ {
+ "title": "Unless recalled by the OSC address",
+ "timeStart": 34920000+60000,
+ "timeEnd": 35520000+60000,
+ "type": "event",
+ "id": "8ee5"
+ },
+ {
+ "title": "Use simpler times to create a timer",
+ "timeStart": 120000+60000,
+ "timeEnd": 720000+60000,
+ "type": "event",
+ "id": "8222"
+ },
+ {
+ "title": "Add delay blocks to affect all events",
+ "timeStart": 37320000+60000+900000,
+ "timeEnd": 38520000+60000+900000,
+ "type": "event",
+ "id": "6dce"
+ },
+ {
+ "title": "Add and remove events with [+] and [-]",
+ "timeStart": 38520000+60000+900000,
+ "timeEnd": 45120000+60000+900000,
+ "type": "event",
+ "id": "2651"
+ },
+ {
+ "title": "And control whether they are public",
+ "timeStart": 46800000,
+ "timeEnd": 57600000,
+ "type": "event",
+ "id": "1358"
+ }
+ ]
+
+ expect(getEventsWithDelay(testData)).toStrictEqual(expected);
+});
+
+describe('getEventsWithDelay edge cases', () => {
+
+ test('given an empty array', () => {
+ const emptyArray = {
+ test: [],
+ expect: [],
+ }
+
+ expect(getEventsWithDelay(emptyArray.test)).toStrictEqual(emptyArray.expect);
+ });
+
+ test('given an undefined object', () => {
+ const withUndefined = {
+ test: undefined,
+ expect: [],
+ }
+
+ expect(getEventsWithDelay(withUndefined.test)).toStrictEqual(withUndefined.expect);
+ });
+
+ test('given a corrupted event object', () => {
+ const testData = [
+ {
+ "title": "Welcome to Ontime",
+ "timeEnd": 30600000,
+ "type": "event",
+ "id": "5946"
+ },
+ {
+ "duration": 60000,
+ "type": "delay",
+ "id": "24240"
+ },
+ {
+ "title": "Unless recalled by the OSC address",
+ "timeStart": 34920000,
+ "timeEnd": 35520000,
+ "type": "event",
+ "id": "8ee5"
+ }
+ ];
+ const expected = [
+ {
+ "title": "Welcome to Ontime",
+ "timeEnd": 30600000,
+ "type": "event",
+ "id": "5946"
+ },
+ {
+ "title": "Unless recalled by the OSC address",
+ "timeStart": 34920000+60000,
+ "timeEnd": 35520000+60000,
+ "type": "event",
+ "id": "8ee5"
+ }
+ ];
+
+ expect(getEventsWithDelay(testData)).toStrictEqual(expected);
+ });
+
+ test('given a corrupted delay object', () => {
+ const testData = [
+ {
+ "title": "Welcome to Ontime",
+ "timeStart": 28800000,
+ "timeEnd": 30600000,
+ "type": "event",
+ "id": "5946"
+ },
+ {
+ "type": "delay",
+ "id": "24240"
+ },
+ {
+ "title": "Unless recalled by the OSC address",
+ "timeStart": 34920000,
+ "timeEnd": 35520000,
+ "type": "event",
+ "id": "8ee5"
+ }
+ ];
+ const expected = [
+ {
+ "title": "Welcome to Ontime",
+ "timeStart": 28800000,
+ "timeEnd": 30600000,
+ "type": "event",
+ "id": "5946"
+ },
+ {
+ "title": "Unless recalled by the OSC address",
+ "timeStart": 34920000,
+ "timeEnd": 35520000,
+ "type": "event",
+ "id": "8ee5"
+ }
+ ];
+
+ expect(getEventsWithDelay(testData)).toStrictEqual(expected);
+ });
+});
+
+describe('test trimEventlist function', () => {
+
+ const limit = 8;
+ const testData = [
+ {id: '1'},
+ {id: '2'},
+ {id: '3'},
+ {id: '4'},
+ {id: '5'},
+ {id: '6'},
+ {id: '7'},
+ {id: '8'},
+ {id: '9'},
+ {id: '10'},
+ {id: '11'},
+ {id: '12'},
+ ];
+
+
+ test('when we use the first item', () => {
+ const selectedId = '1';
+ const expected = [
+ {id: '1'},
+ {id: '2'},
+ {id: '3'},
+ {id: '4'},
+ {id: '5'},
+ {id: '6'},
+ {id: '7'},
+ {id: '8'},
+ ];
+
+ const l = trimEventlist(testData, selectedId, limit);
+ expect(l.length).toBe(limit);
+ expect(l).toStrictEqual(expected);
+ });
+
+ test('when we use the third item', () => {
+ const selectedId = '3';
+ const expected = [
+ {id: '1'},
+ {id: '2'},
+ {id: '3'},
+ {id: '4'},
+ {id: '5'},
+ {id: '6'},
+ {id: '7'},
+ {id: '8'}
+ ];
+
+ const l = trimEventlist(testData, selectedId, limit);
+ expect(l.length).toBe(limit);
+ expect(l).toStrictEqual(expected);
+ });
+
+ test('when we use the fourth item', () => {
+ const selectedId = '4';
+ const expected = [
+ {id: '2'},
+ {id: '3'},
+ {id: '4'},
+ {id: '5'},
+ {id: '6'},
+ {id: '7'},
+ {id: '8'},
+ {id: '9'}
+ ];
+
+ const l = trimEventlist(testData, selectedId, limit);
+ expect(l.length).toBe(limit);
+ expect(l).toStrictEqual(expected);
+ });
+
+ test('if selected is not found', () => {
+ const selectedId = '15';
+ const expected = [
+ {id: '1'},
+ {id: '2'},
+ {id: '3'},
+ {id: '4'},
+ {id: '5'},
+ {id: '6'},
+ {id: '7'},
+ {id: '8'},
+ ];
+
+ const l = trimEventlist(testData, selectedId, limit);
+ expect(l.length).toBe(limit);
+ expect(l).toStrictEqual(expected);
+ });
+});
+
+describe('test formatEvents function', () => {
+ const testEvent = [
+ {
+ "title": "Welcome to Ontime",
+ "subtitle": "Subtitles are useful",
+ "presenter": "cpvalente",
+ "note": "Maybe a running note for the operator?",
+ "timeStart": 28800000,
+ "timeEnd": 30600000,
+ "isPublic": false,
+ "type": "event",
+ "revision": 0,
+ "id": "5946"
+ },
+ {
+ "title": "Unless recalled by the OSC address",
+ "subtitle": "",
+ "presenter": "",
+ "note": "In green, below",
+ "timeStart": 34800000,
+ "timeEnd": 35400000,
+ "isPublic": false,
+ "type": "event",
+ "revision": 0,
+ "id": "8ee5"
+ }
+ ];
+
+ test ('it parses correctly', () => {
+ const selectedId = 'otherEvent';
+ const nextId = 'notHere';
+ const expected = [
+ {
+ id: '5946',
+ time: '08:00 - 08:30',
+ title: 'Welcome to Ontime',
+ isNow: false,
+ isNext: false,
+ },
+ {
+ id: '8ee5',
+ time: '09:40 - 09:50',
+ title: 'Unless recalled by the OSC address',
+ isNow: false,
+ isNext: false,
+ },
+
+ ]
+
+ const parsed = formatEventList(testEvent, selectedId, nextId, true);
+ expect(parsed).toStrictEqual(expected);
+ });
+
+ test ('it handles selected correctly', () => {
+ const selectedId = '5946';
+ const nextId = '8ee5';
+ const expected = [
+ {
+ id: '5946',
+ time: '08:00 - 08:30',
+ title: 'Welcome to Ontime',
+ isNow: true,
+ isNext: false,
+ },
+ {
+ id: '8ee5',
+ time: '09:40 - 09:50',
+ title: 'Unless recalled by the OSC address',
+ isNow: false,
+ isNext: true,
+ },
+
+ ]
+
+ const parsed = formatEventList(testEvent, selectedId, nextId,true);
+ expect(parsed).toStrictEqual(expected);
+ });
+
+ test ('it handles next correctly', () => {
+ const selectedId = '8ee5';
+ const nextId = 'notHere';
+
+ const expected = [
+ {
+ id: '5946',
+ time: '08:00 - 08:30',
+ title: 'Welcome to Ontime',
+ isNow: false,
+ isNext: false,
+ },
+ {
+ id: '8ee5',
+ time: '09:40 - 09:50',
+ title: 'Unless recalled by the OSC address',
+ isNow: true,
+ isNext: false,
+ },
+
+ ]
+
+ const parsed = formatEventList(testEvent, selectedId, nextId, true);
+ expect(parsed).toStrictEqual(expected);
+ });
+})
\ No newline at end of file
diff --git a/client/src/common/utils/dateConfig.js b/client/src/common/utils/dateConfig.js
index 0c95fff74..3bb3af543 100644
--- a/client/src/common/utils/dateConfig.js
+++ b/client/src/common/utils/dateConfig.js
@@ -40,11 +40,11 @@ export const stringFromMillis = (
* another go at simpler string formatting (counters)
* @description Converts seconds to string representing time
* @param {number} seconds - time in seconds
- * @param {boolean} hideZero - wether to show hours in case its 00
+ * @param {boolean} [hideZero] - whether to show hours in case its 00
* @returns {string} String representing absolute time 00:12:02
*/
-export function formatDisplay(seconds, hideZero) {
+export function formatDisplay(seconds, hideZero=false) {
// add an extra 0 if necessary
const format = (val) => `0${Math.floor(val)}`.slice(-2);
@@ -59,7 +59,6 @@ export function formatDisplay(seconds, hideZero) {
/**
* @description Converts milliseconds to seconds
* @param {number} millis - time in seconds
- * @param {boolean} hideZero - wether to show hours in case its 00
* @returns {number} Amount in seconds
*/
@@ -71,7 +70,6 @@ export const millisToSeconds = (millis) => {
/**
* @description Converts milliseconds to seconds
* @param {number} millis - time in seconds
- * @param {boolean} hideZero - wether to show hours in case its 00
* @returns {number} Amount in seconds
*/
diff --git a/client/src/common/utils/eventsManager.js b/client/src/common/utils/eventsManager.js
new file mode 100644
index 000000000..b00db9df4
--- /dev/null
+++ b/client/src/common/utils/eventsManager.js
@@ -0,0 +1,84 @@
+/**
+ * @description From a list of events, returns only events of type event with calculated delays
+ * @param {Object[]} events - given events
+ * @returns {Object[]} Filtered events with calculated delays
+ */
+import {stringFromMillis} from "./dateConfig";
+
+export const getEventsWithDelay = (events) => {
+
+ if (events == null) return [];
+
+ const unfilteredEvents = [...events];
+
+ // Add running delay
+ let delay = 0;
+ for (const e of unfilteredEvents) {
+ if (e.type === 'block') delay = 0;
+ else if (e.type === 'delay') delay = delay + e.duration;
+ else if (e.type === 'event' && delay > 0) {
+ e.timeStart += delay;
+ e.timeEnd += delay;
+ }
+ }
+
+ // filter just events
+ return unfilteredEvents.filter((e) => e.type === 'event');
+};
+
+/**
+ * @description Returns trimmed event list array
+ * @param {Object[]} events - given events
+ * @param {string} selectedId - id of currently selected event
+ * @param {number} limit - max number of events to return
+ * @returns {Object[]} Event list with maximum objects
+ */
+export const trimEventlist = (events, selectedId, limit) => {
+ if (events == null) return [];
+
+ const BEFORE = 2;
+ const trimmedEvents = [...events];
+
+ // limit events length if necessary
+ if (limit != null) {
+ while (trimmedEvents.length > limit) {
+ const idx = trimmedEvents.findIndex((e) => e.id === selectedId);
+ if (idx <= BEFORE) { trimmedEvents.pop(); }
+ else { trimmedEvents.shift(); }
+ }
+ }
+ return trimmedEvents;
+};
+
+/**
+ * @description Returns list of events formatted to be displayed
+ * @param {Object[]} events - given events
+ * @param {string} selectedId - id of currently selected event
+ * @param {string} nextId - id of next event
+ * @param {boolean} [showEnd] - whether to show the end time
+ * @returns {Object[]} Formatted list of events [{time: -, title: -, isNow, isNext}]
+ */
+export const formatEventList = (events, selectedId, nextId, showEnd = false) => {
+ if (events == null) return [];
+
+ const givenEvents = [...events];
+
+ // format list
+ let formattedEvents = [];
+ for (const g of givenEvents) {
+ const start = stringFromMillis(g.timeStart, false);
+ const end = stringFromMillis(g.timeEnd, false);
+
+ formattedEvents.push({
+ id: g.id,
+ time: showEnd ? `${start} - ${end}` : start,
+ title: g.title,
+ isNow: g.id === selectedId,
+ isNext: g.id === nextId,
+ });
+ }
+
+ return formattedEvents;
+};
+
+
diff --git a/client/src/features/control/MessageControl.jsx b/client/src/features/control/MessageControl.jsx
index e881cb5f1..264c4c5f8 100644
--- a/client/src/features/control/MessageControl.jsx
+++ b/client/src/features/control/MessageControl.jsx
@@ -1,15 +1,16 @@
-import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
-import { useEffect, useState } from 'react';
-import { useSocket } from 'app/context/socketContext';
+import {Editable, EditableInput, EditablePreview} from '@chakra-ui/editable';
+import {Switch} from "@chakra-ui/react";
+import {useEffect, useState} from 'react';
+import {useSocket} from 'app/context/socketContext';
import VisibleIconBtn from 'common/components/buttons/VisibleIconBtn';
-import style from './MessageControl.module.css';
+import style from './MessageControl.module.scss';
const inputProps = {
size: 'sm',
};
const InputRow = (props) => {
- const { label, placeholder, text, visible } = props;
+ const {label, placeholder, text, visible} = props;
return (
<>
@@ -22,8 +23,8 @@ const InputRow = (props) => {
className={style.inline}
color={text === '' ? '#666' : 'inherit'}
>
-
-
+
+
{
if (socket == null) return;
// Handle presenter messages
socket.on('messages-presenter', (data) => {
- setPres({ ...data });
+ setPres({...data});
});
// Handle public messages
socket.on('messages-public', (data) => {
- setPubl({ ...data });
+ setPubl({...data});
});
// Handle lower third messages
socket.on('messages-lower', (data) => {
- setLower({ ...data });
+ setLower({...data});
+ });
+
+ // Handle lower third messages
+ socket.on('onAir', (data) => {
+ setonAir(data);
});
// Ask for up to date data
socket.emit('get-messages');
+ // Ask for onAir state
+ socket.emit('get-onAir');
// Clear listeners
return () => {
socket.off('messages-public');
socket.off('messages-presenter');
socket.off('messages-lower');
+ socket.off('onAir');
};
}, [socket]);
@@ -99,38 +109,54 @@ export default function MessageControl() {
case 'toggle-lower-visible':
socket.emit('set-lower-visible', !lower.visible);
break;
-
+ case 'toggle-onAir':
+ socket.emit('set-onAir', !onAir);
+ break;
default:
break;
}
};
return (
-
- messageControl('pres-text', event)}
- actionHandler={() => messageControl('toggle-pres-visible')}
- />
- messageControl('publ-text', event)}
- actionHandler={() => messageControl('toggle-publ-visible')}
- />
- messageControl('lower-text', event)}
- actionHandler={() => messageControl('toggle-lower-visible')}
- />
-
+ <>
+
+ messageControl('pres-text', event)}
+ actionHandler={() => messageControl('toggle-pres-visible')}
+ />
+ messageControl('publ-text', event)}
+ actionHandler={() => messageControl('toggle-publ-visible')}
+ />
+ messageControl('lower-text', event)}
+ actionHandler={() => messageControl('toggle-lower-visible')}
+ />
+
+
+ messageControl('toggle-onAir')}>
+ On Air?
+
+
+ {`/ontime/offAir << OSC >> /ontime/onAir`}
+
+
+ >
);
}
diff --git a/client/src/features/control/MessageControl.module.css b/client/src/features/control/MessageControl.module.css
deleted file mode 100644
index 6c67eeb37..000000000
--- a/client/src/features/control/MessageControl.module.css
+++ /dev/null
@@ -1,32 +0,0 @@
-.messageContainer {
- background-color: rgba(0, 0, 0, 0.05);
- border: 1px solid rgba(0, 0, 0, 0.05);
- border-radius: 4px;
- display: flex;
- flex-direction: column;
- gap: 0.5em;
- padding: 0.5em;
-}
-
-.inputItems {
- display: grid;
- grid-template-columns: 1fr auto;
- gap: 1em;
-}
-
-.label {
- padding: 0;
- margin: 0;
- font-size: 0.9em;
- color: #ccc;
-}
-
-.inline {
- border-radius: 4px;
- background-color: rgba(255, 255, 255, 0.05);
- border: 1px solid rgba(255, 255, 255, 0.05);
-}
-
-.padleft {
- padding-left: 0.5em;
-}
diff --git a/client/src/features/control/MessageControl.module.scss b/client/src/features/control/MessageControl.module.scss
new file mode 100644
index 000000000..52889d3fb
--- /dev/null
+++ b/client/src/features/control/MessageControl.module.scss
@@ -0,0 +1,54 @@
+.messageContainer,
+.onAirToggle {
+ background-color: rgba(0, 0, 0, 0.05);
+ border: 1px solid rgba(0, 0, 0, 0.05);
+ border-radius: 4px;
+ display: flex;
+ gap: 0.5em;
+ padding: 0.5em;
+
+}
+
+.messageContainer {
+ flex-direction: column;
+
+ .inputItems {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ gap: 1em;
+ }
+ .label {
+ padding: 0;
+ margin: 0;
+ font-size: 0.9em;
+ color: #ccc;
+ }
+ .inline {
+ border-radius: 4px;
+ background-color: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ }
+ .padleft {
+ padding-left: 0.5em;
+ }
+}
+
+.onAirToggle {
+ margin-top: 1em;
+ display: flex;
+ gap: 1em;
+ align-items: center;
+ line-height: 3em;
+
+ .onAirLabel {
+ font-size: 1.2em;
+ }
+ .oscLabel {
+ color: #4bffabcc;
+ font-size: 0.8em;
+ float: right;
+ padding-right: 1em;
+ -webkit-user-select: text;
+ user-select: text;
+ }
+}
diff --git a/client/src/features/viewers/ViewWrapper.jsx b/client/src/features/viewers/ViewWrapper.jsx
index 6d90c0a7e..7f58dea19 100644
--- a/client/src/features/viewers/ViewWrapper.jsx
+++ b/client/src/features/viewers/ViewWrapper.jsx
@@ -10,13 +10,9 @@ const withSocket = (Component) => {
const WrappedComponent = (props) => {
const {
data: eventsData,
- status: eventsDataStatus,
- isError: eventsDataIsError,
} = useFetch(EVENTS_TABLE, fetchAllEvents);
const {
data: genData,
- status: genDataStatus,
- isError: genDataIsError,
} = useFetch(EVENT_TABLE, fetchEvent);
const [publicEvents, setPublicEvents] = useState([]);
@@ -58,6 +54,7 @@ const withSocket = (Component) => {
presenterNext: '',
});
const [selectedId, setSelectedId] = useState(null);
+ const [nextId, setNextId] = useState(null);
const [publicSelectedId, setPublicSelectedId] = useState(null);
const [general, setGeneral] = useState({
title: '',
@@ -67,6 +64,7 @@ const withSocket = (Component) => {
endMessage: '',
});
const [playback, setPlayback] = useState(null);
+ const [onAir, setOnAir] = useState(false);
// Ask for update on load
useEffect(() => {
@@ -96,6 +94,9 @@ const withSocket = (Component) => {
socket.on('playstate', (data) => {
setPlayback(data);
});
+ socket.on('onAir', (data) => {
+ setOnAir(data);
+ });
// Handle titles
socket.on('titles', (data) => {
@@ -112,6 +113,9 @@ const withSocket = (Component) => {
socket.on('publicselected-id', (data) => {
setPublicSelectedId(data);
});
+ socket.on('next-id', (data) => {
+ setNextId(data);
+ });
// Ask for up to date data
socket.emit('get-messages');
@@ -124,6 +128,7 @@ const withSocket = (Component) => {
// ask for playstate
socket.emit('get-playstate');
+ socket.emit('get-onAir')
// Ask for up titles
socket.emit('get-titles');
@@ -131,6 +136,7 @@ const withSocket = (Component) => {
// Ask for up selected
socket.emit('get-selected-id');
+ socket.emit('get-next-id');
// Clear listeners
return () => {
@@ -139,9 +145,11 @@ const withSocket = (Component) => {
socket.off('messages-lower');
socket.off('timer');
socket.off('playstate');
+ socket.off('onAir');
socket.off('titles');
socket.off('publictitles');
socket.off('selected-id');
+ socket.emit('next-id');
};
}, [socket]);
@@ -224,6 +232,7 @@ const withSocket = (Component) => {
...timer,
finished: playback === 'start' && timer.running <= 0 && timer.startedAt,
clock: stringFromMillis(timer.clock),
+ clockNoSeconds: stringFromMillis(timer.clock, false),
playstate: playback,
};
@@ -240,7 +249,9 @@ const withSocket = (Component) => {
backstageEvents={backstageEvents}
selectedId={selectedId}
publicSelectedId={publicSelectedId}
+ nextId={nextId}
general={general}
+ onAir={onAir}
/>
);
};
diff --git a/client/src/features/viewers/backstage/StageManager.jsx b/client/src/features/viewers/backstage/StageManager.jsx
index 3e843b2a1..39d507a83 100644
--- a/client/src/features/viewers/backstage/StageManager.jsx
+++ b/client/src/features/viewers/backstage/StageManager.jsx
@@ -6,6 +6,7 @@ import NavLogo from 'common/components/nav/NavLogo';
import { useEffect, useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import TitleSide from 'common/components/views/TitleSide';
+import {getEventsWithDelay} from "../../../common/utils/eventsManager";
export default function StageManager(props) {
const { publ, title, time, backstageEvents, selectedId, general } = props;
@@ -20,23 +21,9 @@ export default function StageManager(props) {
useEffect(() => {
if (backstageEvents == null) return;
- let events = [...backstageEvents];
+ setFilteredEvents(getEventsWithDelay(backstageEvents));
- // Add running delay
- let delay = 0;
- for (const e of events) {
- if (e.type === 'block') delay = 0;
- else if (e.type === 'delay') delay = delay + e.duration;
- else if (e.type === 'event' && delay > 0) {
- e.timeStart += delay;
- e.timeEnd += delay;
- }
- }
-
- // filter just events
- let filtered = events.filter((e) => e.type === 'event');
-
- setFilteredEvents(filtered);
+ console.log('hhh', getEventsWithDelay(backstageEvents))
}, [backstageEvents]);
// Format messages
diff --git a/client/src/features/viewers/presenter/PresenterView.jsx b/client/src/features/viewers/presenter/PresenterView.jsx
index 59957f1f1..7e185b1e1 100644
--- a/client/src/features/viewers/presenter/PresenterView.jsx
+++ b/client/src/features/viewers/presenter/PresenterView.jsx
@@ -11,7 +11,7 @@ export default function PresenterView(props) {
// Set window title
useEffect(() => {
- document.title = 'ontime - Speaker Screen';
+ document.title = 'ontime - Presenter Screen';
}, []);
const showOverlay = pres.text !== '' && pres.visible;
diff --git a/client/src/features/viewers/production/lower/LowerClean.jsx b/client/src/features/viewers/production/lower/LowerClean.jsx
index fb9fc8c40..b26dec608 100644
--- a/client/src/features/viewers/production/lower/LowerClean.jsx
+++ b/client/src/features/viewers/production/lower/LowerClean.jsx
@@ -1,6 +1,7 @@
import { AnimatePresence, motion } from 'framer-motion';
import { useEffect, useState } from 'react';
import style from './LowerClean.module.css';
+import NavLogo from "../../../../common/components/nav/NavLogo";
export default function LowerClean(props) {
const { lower, title, options } = props;
@@ -86,6 +87,9 @@ export default function LowerClean(props) {
fontSize: `${sizeMultiplier}vh`,
}}
>
+
+
+
{showLower && (
+
+
+
{showLower && (
{
+ document.title = 'ontime - Studio Clock';
+ }, []);
+
+ // Prepare event list
+ // Todo: useMemo()
+ useEffect(() => {
+ if (backstageEvents == null) return;
+
+ const events = backstageEvents.filter((e) => e.type === 'event');
+
+ let e = trimEventlist(events, selectedId, MAX_TITLES);
+ e = formatEventList(e, selectedId, nextId);
+ setSchedule(e);
+
+ }, [backstageEvents, selectedId, nextId]);
+
+ return (
+
+
+
+
{time.clockNoSeconds}
+
+ {title.titleNext}
+
+
0 ? style.nextCountdown: style.nextCountdown__overtime}>
+ {selectedId != null && formatDisplay(time.running)}
+
+
+ {hoursIndicators.map(i => (
+
+ )
+ )}
+ {minutesIndicators.map(i => (
+
+ )
+ )}
+
+
+
+
ON AIR
+
+
+ {schedule.map((s) => (
+
+
{`${s.time} ${s.title}`}
+
+ ))
+ }
+
+
+
+
+ );
+}
diff --git a/client/src/features/viewers/studio/StudioClock.module.scss b/client/src/features/viewers/studio/StudioClock.module.scss
new file mode 100644
index 000000000..ebf1f7e53
--- /dev/null
+++ b/client/src/features/viewers/studio/StudioClock.module.scss
@@ -0,0 +1,190 @@
+@font-face {
+ font-family: "digital-clock";
+ src: local('digital-7'), url('./../../../assets/fonts/digital-7.mono.ttf') format('truetype') ;
+}
+
+.container {
+ margin: 0;
+ box-sizing: border-box; /* reset */
+ overflow: hidden;
+ width: 100%; /* restrict the page width to viewport */
+ height: 100vh;
+ padding: 1vw;
+
+ background: #111;
+
+ display: grid;
+ grid-template-columns: 1000px 1fr;
+ gap: 50px;
+ grid-template-areas: "clck schd";
+
+ /* =============== CLOCK STUFF ==================*/
+
+ $clock-size: 900px;
+ $size-hours: 20px;
+ $half-hours: 10px;
+ $size-min: 18px;
+ $half-min: 9px;
+ $red-active: #c53030;
+ $red-idle: darken($red-active, 30%);
+ $cyan-active: #0ff;
+ $cyan-idle: #0aa;
+
+ .clockContainer {
+ display: grid;
+ place-content: center;
+ grid-area: clck;
+ width: $clock-size;
+ height: $clock-size;
+ text-align: center;
+ position: relative;
+ margin: auto;
+ font-family: digital-clock, monospace;
+ text-transform: uppercase;
+
+
+ .time {
+ margin-top: 175px;
+ color: $red-active;
+ font-size: 275px;
+ line-height: 0.8em;
+ text-shadow: rgb(180,0,0) 0 0 20px;
+ }
+ .nextTitle:after,
+ .nextCountdown:after,
+ .nextCountdown__overtime:after {
+ content: '\200b';
+ }
+ .nextTitle {
+ color:$cyan-idle;
+ line-height: 100px;
+ }
+ .nextCountdown,
+ .nextCountdown__overtime {
+ font-size: 100px;
+ line-height: 1em;
+ }
+ .nextCountdown {
+ color: $cyan-active;
+ text-shadow: rgb(0,100,100) 0 0 20px;
+ }
+ .nextCountdown::before {
+ content: '-';
+ }
+ .nextCountdown__overtime {
+ color: darken($red-active, 10%);
+ }
+ .indicators {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+
+ .min,
+ .min__active,
+ .hours,
+ .hours__active {
+ border-radius: 50%;
+ position: absolute;
+ background: $red-idle;
+ }
+ .min,
+ .min__active {
+ min-height: $size-min;
+ width: $size-min;
+ top: calc(50% - #{$half_min});
+ left: calc(50% - #{$half_min});
+ }
+ .hours,
+ .hours__active{
+ min-height:$size-hours;
+ width: $size-hours;
+ top: calc(50% - #{$half_hours});
+ left: calc(50% - #{$half_hours});
+ }
+ .min__active,
+ .hours__active {
+ background: $red-active;
+ box-shadow: 0 0 10px 2px rgba(255,0,0,0.25);
+ }
+ }
+ }
+
+ /* ============= SCHEDULE STUFF =================*/
+
+ .scheduleContainer {
+ grid-area: schd;
+ margin: 50px 0;
+ padding-right: 50px;
+ font-family: digital-clock, monospace;
+ text-transform: uppercase;
+
+
+ .onAir,
+ .onAir__idle{
+ padding-bottom: 50px;
+ font-size: 190px;
+ line-height: 0.9em;
+ letter-spacing: 0.05em;
+ }
+ .onAir {
+ color: $red-active;
+ text-shadow: rgb(150,0,0) 0 0 20px;
+ }
+ .onAir__idle {
+ color: $red-idle;
+ }
+ .schedule {
+
+ ul {
+ color: $cyan-idle;
+ line-height: 1em;
+ list-style: none;
+ font-size: 40px;
+ }
+ li {
+ padding-bottom: 0.5em;
+ display: flex;
+ align-items: center;
+ gap: 20px;
+ }
+ .now {
+ color: $cyan-active;
+ text-shadow: rgb(0,100,100) 0 0 20px;
+ }
+ .next {
+ color: $red-active;
+ text-shadow: rgb(100,0,0) 0 0 20px;
+ }
+ .decorator,
+ .decorator__active,
+ .decorator__next {
+ aspect-ratio: 1;
+ border-radius: 50%;
+ background: $red-idle;
+ min-height: $size-hours;
+ max-height: $size-hours;
+ width: $size-hours;
+ }
+ .decorator__active {
+ background: $cyan-active;
+ box-shadow: 0 0 10px 2px rgba(0,255,255,0.25);
+ }
+ .decorator__next{
+ background: $red-active;
+ box-shadow: 0 0 10px 2px rgba(255,0,0,0.25);
+ }
+ }
+ }
+}
+
+@media only screen and (max-width: 1600px) {
+ .container {
+ display: grid;
+ grid-template-areas: "clck";
+ grid-template-columns: 100%;
+ place-content: center;
+ }
+ .scheduleContainer{
+ display: none;
+ }
+}
diff --git a/client/yarn.lock b/client/yarn.lock
index a15a4edc8..967a81dfc 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -3073,6 +3073,14 @@ anymatch@^3.0.3, anymatch@~3.1.1:
normalize-path "^3.0.0"
picomatch "^2.0.4"
+anymatch@~3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
+ integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
aproba@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
@@ -3990,6 +3998,21 @@ check-types@^11.1.1:
resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f"
integrity sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==
+"chokidar@>=3.0.0 <4.0.0":
+ version "3.5.2"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
+ integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
chokidar@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -6054,7 +6077,7 @@ fsevents@^1.2.7:
bindings "^1.5.0"
nan "^2.12.1"
-fsevents@^2.1.2, fsevents@^2.1.3, fsevents@~2.3.1:
+fsevents@^2.1.2, fsevents@^2.1.3, fsevents@~2.3.1, fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
@@ -6137,7 +6160,7 @@ glob-parent@^3.1.0:
is-glob "^3.1.0"
path-dirname "^1.0.0"
-glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0:
+glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -6608,6 +6631,11 @@ immer@8.0.1:
resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656"
integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
+immutable@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
+ integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
+
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@@ -10366,6 +10394,13 @@ readdirp@~3.5.0:
dependencies:
picomatch "^2.2.1"
+readdirp@~3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+ integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+ dependencies:
+ picomatch "^2.2.1"
+
recursive-readdir@2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"
@@ -10565,6 +10600,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
+resize-observer-polyfill@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+ integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
@@ -10811,6 +10851,14 @@ sass-loader@^10.0.5:
schema-utils "^3.0.0"
semver "^7.3.2"
+sass@^1.44.0:
+ version "1.44.0"
+ resolved "https://registry.yarnpkg.com/sass/-/sass-1.44.0.tgz#619aa0a2275c097f9af5e6b8fe8a95e3056430fb"
+ integrity sha512-0hLREbHFXGQqls/K8X+koeP+ogFRPF4ZqetVB19b7Cst9Er8cOR0rc6RU7MaI4W1JmUShd1BPgPoeqmmgMMYFw==
+ dependencies:
+ chokidar ">=3.0.0 <4.0.0"
+ immutable "^4.0.0"
+
sax@~1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@@ -12131,6 +12179,13 @@ use-callback-ref@^1.2.3, use-callback-ref@^1.2.5:
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5"
integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==
+use-fit-text@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/use-fit-text/-/use-fit-text-2.4.0.tgz#d3d1cd72f6d29cfb2233ec0e0b38c6f25d319f67"
+ integrity sha512-Iy4LMrXcdxWlyZ5phntMpJMgyXGB1p3tV73y2r0QrZ6f/thPh+/QU3ie6RCXmjF8tHMs20FKMPskXeDYIla/Ww==
+ dependencies:
+ resize-observer-polyfill "^1.5.1"
+
use-memo-one@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
diff --git a/server/package.json b/server/package.json
index dca7b404e..3a1099646 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,6 @@
{
"name": "ontime",
- "version": "0.4.1",
+ "version": "0.4.2",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
diff --git a/server/src/classes/EventTimer.js b/server/src/classes/EventTimer.js
index 2224b2d72..c7fc10943 100644
--- a/server/src/classes/EventTimer.js
+++ b/server/src/classes/EventTimer.js
@@ -63,6 +63,7 @@ export class EventTimer extends Timer {
nextPublicEventId = null;
numEvents = null;
_eventlist = null;
+ onAir = false;
constructor(httpServer, oscClient, config) {
// call super constructor
@@ -199,7 +200,7 @@ export class EventTimer extends Timer {
// send current timer
broadcastTimer() {
// through websockets
- this.io.emit('timer', this.getObject());
+ this.io.emit('timer', this.getTimes());
// through OSC, only if running
if (this.state === 'start' || this.state === 'roll') {
@@ -211,7 +212,7 @@ export class EventTimer extends Timer {
// broadcast state
broadcastState(update = true) {
- this.io.emit('timer', this.getObject(update));
+ this.io.emit('timer', this.getTimes(update));
this.io.emit('playstate', this.state);
this.io.emit('selected', {
id: this.selectedEventId,
@@ -224,6 +225,7 @@ export class EventTimer extends Timer {
this.io.emit('publicnext-id', this.nextPublicEventId);
this.io.emit('titles', this.titles);
this.io.emit('publictitles', this.titlesPublic);
+ this.io.emit('onAir', this.onAir);
}
// broadcast message
@@ -359,7 +361,7 @@ export class EventTimer extends Timer {
);
// send state
- socket.emit('timer', this.getObject());
+ socket.emit('timer', this.getTimes());
socket.emit('playstate', this.state);
socket.emit('selected-id', this.selectedEventId);
socket.emit('next-id', this.nextEventId);
@@ -385,7 +387,7 @@ export class EventTimer extends Timer {
/*******************************************/
// general playback state
socket.on('get-state', () => {
- socket.emit('timer', this.getObject());
+ socket.emit('timer', this.getTimes());
socket.emit('playstate', this.state);
socket.emit('selected-id', this.selectedEventId);
socket.emit('next-id', this.nextEventId);
@@ -400,7 +402,7 @@ export class EventTimer extends Timer {
});
socket.on('get-timer', () => {
- socket.emit('timer', this.getObject());
+ socket.emit('timer', this.getTimes());
});
socket.on('increment-timer', (data) => {
@@ -419,6 +421,15 @@ export class EventTimer extends Timer {
socket.emit('playstate', this.state);
});
+ socket.on('set-onAir', (data) => {
+ this.onAir = data;
+ this.broadcastThis('onAir', this.onAir);
+ });
+
+ socket.on('get-onAir', () => {
+ socket.emit('onAir', this.onAir);
+ });
+
/*******************************************/
// selection data
socket.on('get-selected', () => {
@@ -949,6 +960,14 @@ export class EventTimer extends Timer {
`;
}
+ /**
+ * @description Set onAir property of timer
+ * @param {boolean} onAir - whether flag is active
+ */
+ set onAir(onAir) {
+ this.onAir = onAir;
+ }
+
start() {
// if there is nothing selected, no nothing
if (this.selectedEventId == null) return;
diff --git a/server/src/classes/Timer.js b/server/src/classes/Timer.js
index 5ed019a14..8cf6eea38 100644
--- a/server/src/classes/Timer.js
+++ b/server/src/classes/Timer.js
@@ -139,8 +139,8 @@ export class Timer {
return this.duration - this.current;
}
- // getObject
- getObject(update = true) {
+ // get time object
+ getTimes(update = true) {
// update timer
if (update) this.update();
diff --git a/server/src/controllers/OscController.js b/server/src/controllers/OscController.js
index a9a59e1b4..5eff71e31 100644
--- a/server/src/controllers/OscController.js
+++ b/server/src/controllers/OscController.js
@@ -38,6 +38,12 @@ export const initiateOSC = (config) => {
// get second part (command)
switch (path.toLowerCase()) {
+ case 'onAir':
+ global.timer.onAir = true;
+ break;
+ case 'offAir':
+ global.timer.onAir = false;
+ break;
case 'start':
case 'play':
console.log('calling play');
diff --git a/server/src/controllers/playbackController.js b/server/src/controllers/playbackController.js
index 16b3d7f78..0e2477878 100644
--- a/server/src/controllers/playbackController.js
+++ b/server/src/controllers/playbackController.js
@@ -4,6 +4,22 @@ export const pbGet = async (req, res) => {
res.send(global.timer.playState);
};
+// Create controller for GET request to '/playback/onAir'
+// Turns onAir flag to true
+export const onAir = async (req, res) => {
+ console.log('Setting onAir to true');
+ global.timer.onAir = true;
+ res.sendStatus(200);
+};
+
+// Create controller for GET request to '/playback/onAir'
+// Turns onAir flag to true
+export const offAir = async (req, res) => {
+ console.log('Setting onAir to false');
+ global.timer.onAir = false;
+ res.sendStatus(200);
+};
+
// Create controller for GET request to '/playback/start'
// Starts timer object
export const pbStart = async (req, res) => {
diff --git a/server/src/routes/playbackRouter.js b/server/src/routes/playbackRouter.js
index 3b71b9c86..d31d1bc99 100644
--- a/server/src/routes/playbackRouter.js
+++ b/server/src/routes/playbackRouter.js
@@ -7,6 +7,12 @@ const playbackController = require('../controllers/playbackController');
// create route between controller and '/playback/' endpoint
router.get('/', playbackController.pbGet);
+// create route between controller and '/playback/onAir' endpoint
+router.get('/onAir', playbackController.onAir);
+
+// create route between controller and '/playback/offAir' endpoint
+router.get('/offAir', playbackController.offAir);
+
// create route between controller and '/playback/start' endpoint
router.get('/start', playbackController.pbStart);