]> OzVa Git service - shopping-channel/commitdiff
Converted rest of pages to websockets
authorMax Value <greenwoodw50@gmail.com>
Sat, 26 Jul 2025 17:54:30 +0000 (18:54 +0100)
committerMax Value <greenwoodw50@gmail.com>
Sat, 26 Jul 2025 17:54:30 +0000 (18:54 +0100)
18 files changed:
TODO
main.py
static/display.css
static/info.json
static/main.css [new file with mode: 0644]
static/static.json
static/utils.js
teleshopping/admin.py
teleshopping/api/data.py
teleshopping/api/utils.py
teleshopping/pages.py
templates/admin.html
templates/clock.html
templates/display.html [deleted file]
templates/gfx.html
templates/hud.html [new file with mode: 0644]
templates/price.html
templates/timer.html

diff --git a/TODO b/TODO
index cc33ad5de45f7bcdfc8079ff6d4c4db311930877..84c804579179722a1fda6541e05198036b1fc706 100644 (file)
--- a/TODO
+++ b/TODO
@@ -1,5 +1,9 @@
-- websockets for the api
 - update the doomsday clock repo
-- better solution to loading static data
+- better solution to loading static data across the board
        - potentially in the database?
        - could open it up for sqlite browser
+       - could build some web editor to allow the user to change stuff
+- fix the doomsday control pannel to relay straight to the clock (via the server)
+- clean up main GFX css
+- check for websockets being secure (for the shout and clock)
+- something else that i forgot
diff --git a/main.py b/main.py
index 887cdb35096f4f5af583aed9a36ef7d988f1acb6..be3ba99e2b84d3e066b641c19582d43cc2fd7268 100755 (executable)
--- a/main.py
+++ b/main.py
@@ -2,7 +2,6 @@
 
 from flask_socketio import SocketIO, emit
 from flask_httpauth import HTTPBasicAuth
-from datetime import datetime, timezone
 from os import path, environ, system
 from flask_cors import CORS
 from flask import Flask,request
@@ -71,10 +70,13 @@ app.before_request(teleshopping.check_database)
 # if the socket is connecting for the first time, send data
 @socketio.on('connect')
 def initial():
-       print("conected!")
        request.method = "internal"
        emit("apiUpdate", teleshopping.data(), json=True)
 
+@socketio.on('shoutUpdate')
+def shout_broadcast(data):
+       emit("shoutUpdate", data, json=True, broadcast=True)
+
 
 if __name__ == "__main__":
        socketio.run(app, host='127.0.0.1', port=8000, debug=True)
index c85b420985119e13fe463cfd008a06854b068a4d..7a3413584907e7a811ccecaebe0abc23112d4edb 100644 (file)
@@ -39,3 +39,13 @@ body {
 #discountBox.show {
        opacity: 1;
 }
+
+#timer_1 {
+       background-color: green;
+}
+#timer_2 {
+       background-color: purple;
+}
+#timer_3 {
+       background-color: blue;
+}
index 27c46e94762ab61a086a9f4885983ceccbac97f0..89490bda9443f8dc8c728f85a67c7d9e4126d823 100644 (file)
 {
        "shoot": {
-               "date": "24th April 2025",
-               "location": "MediaCity UoS, TV Studio A"
+               "date": "Forever",
+               "location": "In our hearts"
        },
        "crew":[
                {
                        "name": "William Greenwood (T)",
                        "role": "Producer",
                        "phone": "075 9476 8180",
-                       "email": "W.Greenwood@edu.salford.ac.uk",
+                       "email": "greenwoodw50@gmail.com",
                        "absent": false
                },
                {
                        "name": "Finn Downton (T)",
                        "role": "Narrative engineer",
                        "phone": "079 1003 1116",
-                       "email": "E.Downton@edu.salford.ac.uk",
+                       "email": "edendownton@gmail.com",
                        "absent": false
                },
                {
                        "name": "Jack Christian-Sims (T)",
                        "role": "Director",
                        "phone": "075 4297 4087",
-                       "email": "J.Christian-Sims@edu.salford.ac.uk",
+                       "email": "jackchristiansims@gmail.com",
                        "absent": false
                },
                {
                        "name": "Eleanor Haughton",
                        "role": "Vision Mixer",
                        "phone": "074 9127 2723",
-                       "email": "E.V.Haughton@edu.salford.ac.uk",
-                       "absent": false
+                       "email": "",
+                       "absent": true
                },
                {
                        "name": "Heather Digwood (T)",
                        "role": "Floor manager",
                        "phone": "077 8817 3853",
-                       "email": "H.Digwood@edu.salford.ac.uk",
-                       "absent": false
+                       "email": "",
+                       "absent": true
                },
                {
                        "name": "Olivia Gillett (T)",
                        "role": "Floor manager",
                        "phone": "073 6852 4302",
-                       "email": "O.Gillett@edu.salford.ac.uk",
-                       "absent": false
+                       "email": "",
+                       "absent": true
                },
                {
                        "name": "Leo Garside-Holdich",
                        "role": "Camera operator",
                        "phone": "078 7474 0891",
-                       "email": "L.N.Garside-Holdich@edu.salford.ac.uk",
-                       "absent": false
+                       "email": "",
+                       "absent": true
                },
                {
                        "name": "Rebecca Dixon",
                        "role": "Sound operator",
                        "phone": "074 3400 7701",
-                       "email": "R.Dixon2@edu.salford.ac.uk",
-                       "absent": false
-               },
-               {
-                       "name": "Alex Roberts",
-                       "role": "Lighting operator",
-                       "phone": "074 9660 1832",
-                       "email": "A.Roberts17@edu.salford.ac.uk",
-                       "absent": false
-               },
-               {
-                       "name": "Shan Hackwood",
-                       "role": "Engineer",
-                       "phone": "075 3068 9322",
-                       "email": "S.Hackwood@edu.salford.ac.uk",
-                       "absent": false
-               },
-               {
-                       "name": "Megan Troop",
-                       "role": "Output monitor",
-                       "phone": "079 2659 5100",
-                       "email": "M.Troop@edu.salford.ac.uk",
-                       "absent": false
+                       "email": "",
+                       "absent": true
                },
                {
                        "name": "Tegan Blake-Barnard (T)",
                        "role": "Set-design lead",
                        "phone": "075 4893 0872",
-                       "email": "T.blake-barnard@edu.Salford.ac.uk",
-                       "absent": false
-               },
-               {
-                       "name": "Lydia Wilkinson",
-                       "role": "Set-design",
-                       "phone": "078 0269 0037",
-                       "email": "L.wilkinson14@edu.salford.ac.uk",
-                       "absent": false
-               },
-               {
-                       "name": "Leigha Blanchard",
-                       "role": "Set-design / GFX operator (pricing)",
-                       "phone": "074 7700 0501",
-                       "email": "L.blanchard@salford.ac.uk",
-                       "absent": false
-               },
-               {
-                       "name": "Daisy Devoe",
-                       "role": "Set-design / GFX operator (timer)",
-                       "phone": "079 7056 4500",
-                       "email": "D.Devoe@edu.salford.ac.uk",
-                       "absent": false
-               },
-               {
-                       "name": "Brynn Yates",
-                       "role": "GFX operator (text)",
-                       "phone": "073 7766 9812",
-                       "email": "M.Yates5@edu.salford.ac.uk",
-                       "absent": false
-               },
-               {
-                       "name": "Ben Cousen",
-                       "role": "GFX operator",
-                       "phone": "079 1333 5446",
-                       "email": "B.Cousen@edu.salford.ac.uk",
-                       "absent": false
+                       "email": "",
+                       "absent": true
                },
                {
                        "name": "Eli Clack",
                        "role": "Photographer",
                        "phone": "079 0824 2349",
-                       "email": "G.Clack@edu.salford.ac.uk",
-                       "absent": false
+                       "email": "",
+                       "absent": true
                }
        ],
        "cast":[
                        "name": "David Smith",
                        "role": "Anchor",
                        "phone": "078 8839 8003",
-                       "email": "D.Smith51@edu.salford.ac.uk",
-                       "absent": false
-               },
-               {
-                       "name": "Lucas Blackburn",
-                       "role": "Backup Anchor",
-                       "phone": "075 9324 5021",
-                       "email": "ext",
+                       "email": "",
                        "absent": false
                }
        ],
        "schedule":[
                {
                        "time": "0900",
-                       "title": "All crew call time for briefing.",
-                       "extra": ["Room 3.08 MediaCity"]
-               },
-               {
-                       "time": "0930",
-                       "title": "Kit picked up from the MediaCity kit store",
+                       "title": "Things happen all the time",
                        "extra": [
-                               "To be picked up by the vision mixer unless otherwise specified.",
-                               "Kit is: GoPro Hero Kit Bundle (2), Zoom F6 (1), Radio mic (3)"
-                       ]
-               },
-               {
-                       "time": "1000",
-                       "title": "All crew moves to Studio A",
-                       "extra": [
-                               "Vision mixer, Engineer, Sound, Output monitor and dedicated GFX operators to gallery to setup the cameras and sound kit as instructed by producer in advance.",
-                               "All other personnel to the floor to setup the set. Floor plan is present on the digital docs, both FMs and the Set design lead will have tablets to access this, but the link will work on any device.",
-                               "Lighting to setup and test lighting.",
-                               "As soon as set complete: technical test on all kit including THE THREAT, the Lazy-Susan, the Doomsday Clock, the GFX system and the sound system.",
-                               "Gallery crew should start technical test of the GFX system and the sound system at the instruction of the Narrative Engineer if the producer is still on the floor.",
-                               "Set-design team begin bringing down the products from room 3.08 at the direction of set design lead"
-                       ]
+                               "Things end too",
+                               "Things change",
+                               "Did you ever poorly mix instant coffee?",
+                               "Little specks of black",
+                               "Against milky cream"
+                               ]
                },
                {
                        "time": "1030",
-                       "title": "Crew 15 minute break",
-                       "extra": [
-                       ]
-               },
-               {
-                       "time": "1045",
-                       "title": "Crew arrive back on set: begin run-throughs of the end",
-                       "extra": [
-                               "Call time for photographer",
-                               "Full crew run-throughs until happy with the order of operations",
-                               "Should include THE THREAT, so The Anchor must not be present.",
-                               "See production notes document for more details"
-                       ]
-               },
-               {
-                       "time": "1100",
-                       "title": "Anchor arrives, is met by set-design lead in room 3.08 for costuming.",
-                       "extra": []
-               },
-               {
-                       "time": "1130",
-                       "title": "Prepare for Anchor on the floor",
+                       "title": "If you invert the colors it looks like stars",
                        "extra": [
-                               "Crew take position for short practice"
-                       ]
+                               "Like a coffee galaxy!",
+                               "Every time I had a coffee I'd flip the colors",
+                               "Look into the coffee night's sky"
+                               ]
                },
                {
                        "time": "1145",
-                       "title": "Practices begin at the direction of the producer",
-                       "extra": [
-                               "This will be practices of product hand-over with the FMs as well as individual products where necessary."
-                       ]
-               },
-               {
-                       "time": "1200",
-                       "title": "The Backup Anchor arrives, is met by set-design lead and producer in the foyer and taken to room 3.08 for costuming and short brief on timings.",
+                       "title": "I dont feel like this happens anymore",
                        "extra": [
-                               "This will be practices of product hand-over with the FMs as well as individual products where necessary.",
-                               "After backup is costumed, to be taken to the green room."
-                       ]
+                               "Maybe they changed how they make coffee",
+                               "Things change",
+                               "It just looks uniform brown now",
+                               "I suppose reversed its like I'm the stars themselves"
+                               ]
                },
                {
                        "time": "1215",
-                       "title": "All crew and cast prepare for full run-through",
+                       "title": "Lunchtime",
                        "extra": []
-               },
-               {
-                       "time": "1230",
-                       "title": "Stream goes live and full run-through begins (latest 1245 for 1h runtime)",
-                       "extra": [
-                               "Backup anchor to be ready to step in for their product when prompted by the FMs"
-                       ]
-               },
-               {
-                       "time": "1345",
-                       "title": "Last possible time a stream can be started with 30 minute runtime (contingency)",
-                       "extra": []
-               },
-               {
-                       "time": "1330",
-                       "title": "Stream ends",
-                       "extra": [
-                               "All crew to the floor for strike",
-                               "When set is de-rigged, all available cast and crew to 3.08 for final words."
-                       ]
                }
        ],
        "notes": [
-               "Sioux Sharp (S.Sharp3@salford.ac.uk) and Joe Fowler (J.A.R.Fowler2@salford.ac.uk) will also be in attendance in the gallery, on the floor, or in the green room. To be confirmed with them.",
-               "Lucas Blackburn's presence (the backup) is to be kept secret from the Anchor, his entrance to the studio is to be facilitated by the cyclorama blocking the Anchors view of the door."
+               "Rebuilding"
                ]
 }
diff --git a/static/main.css b/static/main.css
new file mode 100644 (file)
index 0000000..3c2369a
--- /dev/null
@@ -0,0 +1,499 @@
+body {
+       --magenta: purple;
+       --blue: violet;
+       --red: red;
+
+       font-family: "Archivo", sans-serif;
+       font-size: 1.6vh;
+       color: var(--black);
+
+       opacity: 0;
+       transition: opacity 1.5s;
+
+       overflow: hidden;
+}
+@keyframes spin {
+       0% {transform: rotate(0turn) scale(1.2);}
+       25% {transform: rotate(0.25turn) scale(1);}
+       50% {transform: rotate(0.5turn) scale(1.2);}
+       75% {transform: rotate(0.75turn) scale(1);}
+       100% {transform: rotate(1turn) scale(1.2);}
+}
+@keyframes spin1 {
+       0% {transform: perspective(100px) rotateY( -25deg ) rotateX(-25deg) rotateZ(0turn) scale(1.2);}
+       25% {transform: perspective(100px) rotateY( -25deg ) rotateX(-25deg) rotateZ(0.25turn) scale(1);}
+       50% {transform: perspective(100px) rotateY( -25deg ) rotateX(-25deg) rotateZ(0.5turn) scale(1.2);}
+       75% {transform: perspective(100px) rotateY( -25deg ) rotateX(-25deg) rotateZ(0.75turn) scale(1);}
+       100% {transform: perspective(100px) rotateY( -25deg ) rotateX(-25deg) rotateZ(1turn) scale(1.2);}
+}
+@keyframes spin2 {
+       0% {transform: perspective(100px) rotateY( 20deg ) rotateX(-10deg) rotateZ(0turn) scale(1.2);}
+       25% {transform: perspective(100px) rotateY( 20deg ) rotateX(-10deg) rotateZ(0.25turn) scale(1);}
+       50% {transform: perspective(100px) rotateY( 20deg ) rotateX(-10deg) rotateZ(0.5turn) scale(1.2);}
+       75% {transform: perspective(100px) rotateY( 20deg ) rotateX(-10deg) rotateZ(0.75turn) scale(1);}
+       100% {transform: perspective(100px) rotateY( 20deg ) rotateX(-10deg) rotateZ(1turn) scale(1.2);}
+}
+@keyframes spin3 {
+       0% {transform: perspective(100px) rotateY( -20deg ) rotateX(10deg) rotateZ(0turn) scale(1.2);}
+       25% {transform: perspective(100px) rotateY( -20deg ) rotateX(10deg) rotateZ(0.25turn) scale(1);}
+       50% {transform: perspective(100px) rotateY( -20deg ) rotateX(10deg) rotateZ(0.5turn) scale(1.2);}
+       75% {transform: perspective(100px) rotateY( -20deg ) rotateX(10deg) rotateZ(0.75turn) scale(1);}
+       100% {transform: perspective(100px) rotateY( -20deg ) rotateX(10deg) rotateZ(1turn) scale(1.2);}
+}
+@keyframes spin4 {
+       0% {transform: perspective(100px) rotateY( 25deg ) rotateX(25deg) rotateZ(0turn) scale(1.2);}
+       25% {transform: perspective(100px) rotateY( 25deg ) rotateX(25deg) rotateZ(0.25turn) scale(1);}
+       50% {transform: perspective(100px) rotateY( 25deg ) rotateX(25deg) rotateZ(0.5turn) scale(1.2);}
+       75% {transform: perspective(100px) rotateY( 25deg ) rotateX(25deg) rotateZ(0.75turn) scale(1);}
+       100% {transform: perspective(100px) rotateY( 25deg ) rotateX(25deg) rotateZ(1turn) scale(1.2);}
+}
+@keyframes spinOffset {
+       0% {transform: perspective(200px) translate(-50%, -50%) rotateY( 15deg ) rotateZ(0turn) scale(1.2);}
+       25% {transform: perspective(250px) translate(-50%, -50%) rotateY( 15deg ) rotateZ(0.25turn) scale(1);}
+       50% {transform: perspective(300px) translate(-50%, -50%) rotateY( 15deg ) rotateZ(0.5turn) scale(1.2);}
+       75% {transform: perspective(250px) translate(-50%, -50%) rotateY( 15deg ) rotateZ(0.75turn) scale(1);}
+       100% {transform: perspective(200px) translate(-50%, -50%) rotateY( 15deg ) rotateZ(1turn) scale(1.2);}
+}
+@keyframes spinText {
+       0% {transform: rotate(-0.01turn);}
+       50% {transform: rotate(0.01turn);}
+       100% {transform: rotate(-0.01turn);}
+}
+@keyframes spinText1 {
+       0% {transform: perspective(100px) rotateY( 20deg ) rotateX(-10deg) rotateZ(-0.01turn);}
+       50% {transform: perspective(100px) rotateY( 20deg ) rotateX(-10deg) rotateZ(0.01turn);}
+       100% {transform: perspective(100px) rotateY( 20deg ) rotateX(-10deg) rotateZ(-0.01turn);}
+}
+@keyframes spinText2 {
+       0% {transform: perspective(100px) rotateY( -20deg ) rotateZ(-0.01turn);}
+       50% {transform: perspective(100px) rotateY( -20deg ) rotateZ(0.01turn);}
+       100% {transform: perspective(100px) rotateY( -20deg ) rotateZ(-0.01turn);}
+}
+@keyframes spinText3 {
+       0% {transform: perspective(100px) rotateY( 20deg ) rotateZ(-0.01turn);}
+       50% {transform: perspective(100px) rotateY( 20deg ) rotateZ(0.01turn);}
+       100% {transform: perspective(100px) rotateY( 20deg ) rotateZ(-0.01turn);}
+}
+@keyframes spinText4 {
+       0% {transform: perspective(100px) rotateY( -20deg ) rotateX(10deg) rotateZ(-0.01turn);}
+       50% {transform: perspective(100px) rotateY( -20deg ) rotateX(10deg) rotateZ(0.01turn);}
+       100% {transform: perspective(100px) rotateY( -20deg ) rotateX(10deg) rotateZ(-0.01turn);}
+}
+@keyframes spinTextOffset {
+       0% {transform: perspective(500px) translateX(-50%) rotateY( -10deg ) rotateZ(-0.01turn);}
+       50% {transform: perspective(500px) translateX(-50%) rotateY( -10deg) rotateZ(0.01turn);}
+       100% {transform: perspective(500px) translateX(-50%) rotateY( -10deg ) rotateZ(-0.01turn);}
+}
+@keyframes flicker {
+       0% {
+               color: black;
+               text-shadow: -2px -2px 0 #FFF, 2px -2px 0 #FFF, -2px 2px 0 #FFF, 2px 2px 0 #FFF;
+       }
+       49.999% {
+               color: black;
+               text-shadow: -2px -2px 0 #FFF, 2px -2px 0 #FFF, -2px 2px 0 #FFF, 2px 2px 0 #FFF;
+       }
+       50% {
+               color: white;
+               text-shadow: -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000, 2px 2px 0 #000;
+       }
+       99.999% {
+               color: white;
+               text-shadow: -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000, 2px 2px 0 #000;
+       }
+       100% {
+               color: black;
+               text-shadow: -2px -2px 0 #FFF, 2px -2px 0 #FFF, -2px 2px 0 #FFF, 2px 2px 0 #FFF;
+       }
+}
+
+.container {
+       position: absolute;
+}
+/*.box {
+ *   box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 3px, rgba(0, 0, 0, 0.24) 0px 1px 2px;
+ *   background-image: var(--background);
+ } **/
+#side {
+left: 6vh;
+top: 6vh;
+width: 40vh;
+opacity: 0;
+transition: opacity 1.5s;
+}
+.soldBox {
+       margin: 5px;
+       padding: 5px;
+       color: blue;
+       background-color: white;
+       box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 3px, rgba(0, 0, 0, 0.19) 0px 1px 2px;
+}
+.soldBox > * {
+       margin: 0;
+       font-style: italic;
+}
+.soldBox > * > span {
+       font-weight: bold;
+       font-style: normal;
+}
+#raiting {
+margin: 5px;
+margin-top: 0;
+font-size: 2em;
+line-height: 1em;
+color: white;
+background-color: var(--magenta);
+box-shadow: pink 0px 1px 3px, pink 0px 1px 2px;
+}
+.main {
+       background-color: white;
+       z-index: 0;
+}
+#subtext {
+color: white;
+background-color: var(--blue);
+}
+#banner {
+right: 6vh;
+top: 6vh;
+height: 2em;
+width: 0vh;
+padding: 0.5vh;
+overflow: hidden;
+opacity: 0;
+transition: opacity 1.5s, width 1.5s, left 1.5s;
+color: white;
+background-color: var(--magenta);
+}
+#timer2.moved {
+top: 6vh;
+}
+#timer3.focus {
+bottom: calc(50% - 15vh);
+right: calc(50% - 15vh);
+width: 30vh;
+height: 30vh;
+border-radius: 15vh;
+font-size: 5em;
+line-height: 30vh;
+}
+.extra {
+       position: relative;
+       top: -2vh;
+       z-index: -1;
+       transform: translateY(-100%);
+       padding-top: 2vh;
+       padding-bottom: 1px;
+       color: white;
+       background-color: var(--magenta);
+       transition: transform 1.5s;
+}
+.showExtra > .extra {
+       transform: translateY(0%);
+}
+.main > *, .extra > *:not(.soldBox), .bottom > *, .bottom > div > *, #marquee > * {
+       margin: 0;
+       padding: 0.5vh 1vh;
+}
+#banner > h1 {
+width: 49vh;
+margin: 0;
+font-size: 2em;
+line-height: 1;
+font-style: italic;
+font-weight: bold;
+text-align: center;
+}
+#currentPrice {
+color: var(--red);
+font-size: 2em;
+}
+#unitsLeft {
+font-size: 3em;
+}
+#badge1Container {
+position: relative;
+padding: 0;
+transform: translateX(12vh) scale(0%);
+transition: transform 1.5s;
+}
+#badge1Container > img {
+position: absolute;
+top: -9vh;
+left: 36vh;
+width: 12vh;
+height: 12vh;
+padding: 0;
+animation: spin1 12s linear 0s infinite;
+animation-direction: reverse;
+}
+#badge1Text {
+position: absolute;
+top: -7.5vh;
+left: 31.5vh;
+color: yellow;
+width: 22vh;
+animation: spinText1 7s linear 0s infinite;
+rotate: -0.015turn;
+font-size: 2em;
+text-align: center;
+/*text-shadow: -2px -2px 0 green, 2px -2px 0 green, -2px 2px 0 green, 2px 2px 0 green;*/
+}
+#badge2Container {
+position: relative;
+padding: 0;
+transform: translateX(12vh) scale(0%);
+transition: transform 1.5s;
+}
+#badge2Container > img {
+position: absolute;
+top: -2vh;
+left: 32vh;
+width: 10vh;
+height: 10vh;
+padding: 0;
+animation: spin2 10s linear 0s infinite;
+}
+#badge2Text {
+position: absolute;
+top: -1vh;
+left: 26.5vh;
+color: black;
+width: 20vh;
+animation: spinText2 10s linear 0s infinite;
+rotate: 0.025turn;
+text-align: center;
+/*text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;*/
+}
+#badge3Container {
+position: relative;
+padding: 0;
+transform: translateX(12vh) scale(0%);
+transition: transform 1.5s;
+}
+#badge3Container > img {
+position: absolute;
+top: 3vh;
+left: 38vh;
+width: 10vh;
+height: 10vh;
+padding: 0;
+animation: spin3 10s linear 0s infinite;
+animation-direction: reverse;
+}
+#badge3Text {
+position: absolute;
+top: 4.9vh;
+left: 33.9vh;
+color: blue;
+width: 20vh;
+animation: spinText3 10s linear 0s infinite;
+rotate: -0.03turn;
+text-align: center;
+/*text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;*/
+}
+#badge4Container {
+position: relative;
+padding: 0;
+transform: translateX(12vh) scale(0%);
+transition: transform 1.5s;
+}
+#badge4Container > img {
+position: absolute;
+top: 10vh;
+left: 32vh;
+width: 14vh;
+height: 14vh;
+padding: 0;
+animation: spin4 10s linear 0s infinite;
+}
+#badge4Text {
+position: absolute;
+top: 11.8vh;
+left: 28.4vh;
+color: #00FF00;
+width: 20vh;
+animation: spinText4 10s linear 0s infinite;
+rotate: 0.025turn;
+font-size: 2.2em;
+line-height: 0.6em;
+text-align: center;
+/*text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;*/
+}
+#shoutContainer {
+position: absolute;
+top: 50vh;
+left: 50vw;
+padding: 0;
+}
+#shoutBadgeContainer {
+opacity: 0;
+transform: translateX(-30vw);
+transition: opacity 2s ease, transform 2s ease;
+}
+#shoutContainer.show > #shoutBadgeContainer {
+opacity: 1;
+transform: translateX(0vw);
+}
+#shoutTextContainer {
+position: absolute;
+top: -7vh;
+opacity: 0;
+transform: translateX(30vw);
+transition: opacity 2s ease, transform 2s ease;
+animation: flicker 1s linear 0s infinite;
+}
+#shoutContainer.show > #shoutTextContainer {
+opacity: 1;
+transform: translateX(0vw);
+}
+#shoutBadgeContainer > img {
+width: 45vh;
+height: 45vh;
+padding: 0;
+animation: spinOffset 10s linear 0s infinite;
+}
+#shout {
+position: absolute;
+width: 100vw;
+top: 0;
+perspective: 400px;
+animation: spinTextOffset 10s linear 0s infinite;
+rotate: 0.025turn;
+font-size: 10em;
+text-align: center;
+}
+#origionalPrice {
+line-height: 0.5em;
+}
+#monthlyPrice {
+line-height: 1em;
+}
+.main > hr, .extra > hr {
+       margin: 0 1vh;
+       padding: 0;
+       border-color: var(--dark);
+}
+.bottom {
+       left: 6vh;
+       bottom: 6vh;
+       width: 100vh;
+       background-color: white;
+}
+.bottom > *:not(#timer1) {
+       display: inline-block;
+       vertical-align: top;
+}
+#topTextBox {
+width: calc(100% - 20px);
+margin: 5px;
+margin-bottom: 0;
+padding: 5px;
+color: white;
+background-color: var(--blue);
+box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 3px, rgba(0, 0, 0, 0.19) 0px 1px 2px;
+}
+#topText {
+display: inline-block;
+width: 50%;
+padding: 0;
+}
+#idText {
+display: inline-block;
+width: 50%;
+padding: 0;
+text-align: right;
+font-size: 1.5em;
+font-weight: bold;
+font-style: italic;
+text-shadow: 0 0 3px rgba(0,0,0,0.25);
+}
+#marquee {
+padding: 0;
+width: 100%;
+mask-image: linear-gradient(0.25turn, transparent, black 5%, black 95%, transparent);
+color: var(--magenta);
+overflow: hidden;
+}
+#marqueeText {
+white-space: nowrap;
+overflow: hidden;
+}
+#timer1 {
+position: absolute;
+left: 101vh;
+padding: 0.5vh;
+opacity: 0;
+text-align: center;
+color: white;
+background-color: var(--magenta);
+transition: opacity 1.5s;
+}
+#timer2 {
+position: absolute;
+right: calc(6vh + 2px);
+top: calc(11.5vh + 2px);
+height: 1.2em;
+width: 3em;
+margin: 0;
+text-align: center;
+color: black;
+font-size: 2em;
+font-weight: bold;
+line-height: 1.2em;
+opacity: 0;
+transition: opacity 1.5s, left 1.5s, top 1.5s;
+}
+#timer3 {
+position: absolute;
+bottom: 4vh;
+right: 4vh;
+opacity: 0;
+width: 12vh;
+height: 12vh;
+border-radius: 6vh;
+margin: 0;
+color: white;
+text-align: center;
+vertical-align: center;
+font-family: "Calculator";
+font-size: 2em;
+font-weight: bold;
+line-height: 12vh;
+opacity: 0;
+transition: opacity 1.5s, bottom 1.5s, right 1.5s, width 1.5s, height 1.5s, border-radius 1.5s, font-size 1.5s, line-height 1.5s, transform 1.5s;
+animation: spinText 10s linear 0s infinite;
+}
+.show {
+       opacity: 1 !important;
+}
+#badge1Container.show, #badge2Container.show, #badge3Container.show, #badge4Container.show {
+transform: translateX(0vh) scale(100%);
+}
+#banner.show {
+width: 50vh;
+}
+#sigilBox {
+opacity: 0;
+mix-blend-mode: overlay;
+}
+#sigilBox.show {
+opacity: 1;
+}
+.sigil {
+       position: fixed;
+       top: 50vh;
+       left: 50vw;
+       height: 90vh;
+       transform: translate(-50%, -50%);
+       opacity: 0;
+}
+.sigil.show {
+       opacity: 1;
+}
+@keyframes spinBox {
+       0% {transform: translateY(100%);}
+       50% {}
+       100% {}
+
+}
index 1aa900911b261d00cd34a806eccf5919e0afac74..e83128faf25f4a3e31286134b8b52d3889df33f6 100755 (executable)
                        "Look",
                        "Doomsday",
                        "It's here!"
-               ],
-               "sigil":[
-                       "anchor",
-                       "thebeast",
-                       "buyitall",
-                       "consumer",
-                       "runners",
-                       "xmdv",
-                       "broadcast",
-                       "bleedout",
-                       "quant",
-                       "bullion",
-                       "adarksun",
-                       "dollar",
-                       "beast"
-               ],
-               "square":[
-                       "110,10",
-                       "90,110",
-                       "50,10",
-                       "70,110",
-                       "30,110",
-                       "10,10",
-                       "10,30",
-                       "90,30",
-                       "70,90",
-                       "50,90",
-                       "30,30",
-                       "110,90",
-                       "110,70",
-                       "30,50",
-                       "70,50",
-                       "50,50",
-                       "90,70",
-                       "10,70",
-                       "10,50",
-                       "30,70",
-                       "70,70",
-                       "50,70",
-                       "90,50",
-                       "110,50",
-                       "10,90",
-                       "90,90",
-                       "50,30",
-                       "70,30",
-                       "30,90",
-                       "110,30",
-                       "110,110",
-                       "30,10",
-                       "50,110",
-                       "70,10",
-                       "90,10",
-                       "10,110"
                ]
        },
        "items": [
index 828e96f7e35c426d7f9fdf6c6a5867abc2d521fd..8cc47a288771bbc540a971dc6fbabb50d4e66d46 100644 (file)
@@ -1,16 +1,25 @@
-export function makeTime(end, offset, strike) {
-       let current = Math.round((Date.now() + offset) / 1000);
-       var time = end - current;
+export function makeTime(end) {
+       let current = Date.now() / 1000;
+       var time = parseFloat(end) - current;
 
        if (Math.sign(time) == -1) {time = 0;}
        var minutes = Math.floor(time / 60);
-       var seconds = (time - (minutes * 60));
+       var seconds = Math.floor(time - (minutes * 60));
 
        var minutesString = minutes.toString().padStart(2, "0");
        var secondsString = seconds.toString().padStart(2, "0");
-       if (strike) {
-               return `${minutesString}:${secondsString}`;
-       } else {
-               return `<s>${minutesString}:${secondsString}</s>`;
-       }
+
+       return `${minutesString}:${secondsString}`;
+}
+
+export function getTime(end) {
+       let current = Date.now() / 1000;
+       var time = parseFloat(end) - current;
+
+       if (Math.sign(time) == -1) {time = 0;}
+       var minutes = Math.floor(time / 60);
+       var secondsFull = time - (minutes * 60);
+       var seconds = Math.floor(secondsFull);
+
+       return [minutes, seconds, secondsFull]
 }
index ad1d1d0463f46e61fa85fd29ef1c7fab20b3c4d1..c409b33862c9cfd8cfbb0fc30144b47d75ad3691 100644 (file)
@@ -1,4 +1,5 @@
 from flask import Flask, Response, request, render_template, send_from_directory
+from datetime import datetime, timezone
 from math import radians, cos, sin
 from .api.data import data as api
 from flask_socketio import emit
index 7be9dc37da17e0b25cf9f013770198f1edc0cef4..47c057143cc9096c7bb19268cbf29c9b790abf2d 100644 (file)
@@ -10,37 +10,32 @@ with open(f"{ROOT}/static/static.json", "r", encoding="utf-8") as f:
        static_data = json.loads(f.read())
 
 def data():
-       if request.method in ["GET", "internal"]:
-               data = database("*")
-
-               focus_extra = {}
-               for key, value in data.items():
-                       if key[:4] == "bool": data[key] = bool(value) # if the key starts with "bool" make the data a bool
-                       elif key[:4] == "list": data[key] = literal_eval(value) # if the key starts with "list" make the data a literal list
-                       elif key[:5] == "focus": # if the key starts with "focus" make the data into a bool with an additional dict entry for "showing" bool
-                               data[key] = bool((value-1)+abs(value-1)) # if 2 then True, if 1,0 then False
-                               focus_extra[f"bool_{key[6:]}"] = bool(value) # if 1,2 then True, if 0 then False
-               data |= focus_extra
-
-               if request.method == "internal": return data
-               return jsonify(data)
-       else:
-               return "", 404
-
-def handle_message(message):
-    send(message)
+       data = database("*")
 
-def items():
-       if request.method == "GET":
-               return jsonify(static_data)
+       focus_extra = {}
+       for key, value in data.items():
+               if key[:4] == "bool":
+                       # if the key starts with "bool" make the data a bool
+                       data[key] = bool(value)
 
-       else:
-               return "", 404
+               elif key[:4] == "list":
+                       # if the key starts with "list" make the data a literal list
+                       data[key] = literal_eval(value)
 
-def clock():
-       if request.method == "GET":
-               keys = ["current_position", "movement_speed", "movement_function"]
-               return jsonify(database(keys))
+               elif key[:5] == "focus":
+                       # if the key starts with "focus" change the data from a tertiary value representing
+                       # whether the element is showing and whether the element is currently focused
+                       data[key] = bool((value-1)+abs(value-1)) # if 2 then True, if 1,0 then False
+                       focus_extra[f"bool_{key[6:]}"] = bool(value) # if 1,2 then True, if 0 then False
+
+       data |= focus_extra
+
+       if request.method == "internal": return data
+       return jsonify(data)
 
-       else:
-               return "", 404
+def items():
+       return jsonify(static_data)
+
+def clock():
+       keys = ["current_position", "movement_speed", "movement_function"]
+       return jsonify(database(keys))
index b29cf1b1cb549c2e30985d4d6ddc2edccd77a2fc..8144e649ef91735d7e80783a193d717266302d35 100644 (file)
@@ -45,10 +45,8 @@ def docs_generate():
        with open(f"{docs_path}/documentation.html", "w", encoding="utf-8") as f:
                f.write(render_template("docs.html", data=static_data, info=static_info, book=True))
 
-       floorplan_path = f"{ROOT}/static/floorplan.png"
-       cameras_path = f"{ROOT}/static/cameras.png"
-       system(f"cp '{floorplan_path}' '{docs_path}'")
-       system(f"cp '{cameras_path}' '{docs_path}'")
+       system(f"cp '{ROOT}/static/floorplan.png' '{docs_path}'")
+       system(f"cp '{ROOT}/static/cameras.png' '{docs_path}'")
 
        for book_type in ["epub", "mobi"]:
                system(f"""
index cc56076f03b49bd1cb89e2f14f898c2459c71b40..fc67b3ff2581a9ebd45a484deccc26bd6283c455 100644 (file)
@@ -30,7 +30,7 @@ def autocue():
                )
 
 def hud():
-       return Response(render_template("display.html"), mimetype="text/html")
+       return Response(render_template("hud.html"), mimetype="text/html")
 
 def sounds():
        return Response(render_template("sounds.html"), mimetype="text/html")
index 0eb6c93ecd3d28087c1dd65284da3909894bb2a8..278e664362f3fd074e1e61042819bc5c6d890314 100644 (file)
@@ -13,7 +13,7 @@
                        <ul>
                                <li><a href="/">GFX Main page</a></li>
                                <li><a href="/autocue">Autocue</a></li>
-                               <li><a href="/display">Information display</a></li>
+                               <li><a href="/hud">Information display</a></li>
                        </ul>
                </fieldset>
                <fieldset>
index 114892cf4da4183ec6c721df9ab9f6882404f0c7..b6facb7dfc7ceed6a9df421fe3462709bab242d9 100644 (file)
@@ -4,6 +4,23 @@
                <meta charset="utf-8">
                <title>XMDV Admin - Doomsday</title>
                <link rel="icon" type="image/x-icon" href="/static/assets/star2.svg">
+               <script type="module">
+
+import { io } from "https://cdn.socket.io/4.8.1/socket.io.esm.min.js";
+
+/*
+websocket updates when data is sent to the admin pannel, updating the autocue
+this eliminates the ovehead that comes from constantly polling the api.
+on connection, the system also sends the api data.
+*/
+
+var socket = io('/');
+socket.on('apiUpdate', function (data) {
+       console.info("Recived data, updating...");
+       update(data);
+});
+
+               </script>
                <style>
 /*
 clock styling
@@ -116,7 +133,7 @@ media query to make sure the clock is always displayed well
 
                </style>
        </head>
-       <body onload="update();mainTimer({{data.current_position}});">
+       <body onload="mainTimer({{data.current_position}});">
                <a href="/admin">.. Back to admin pannel</a>
                <form action="/admin/clock" method="POST">
                        <input type="checkbox" value="0" name="end_timer_main" checked="checked" style="display: none">
@@ -178,66 +195,60 @@ function mainTimer (value) {
        else {checkbox.checked = false;}
 }
 
-function update() {
-       fetch("/api", {cache: "no-store"})
-               .then(data => data.json())
-               .then(data => {
-                       for (let t = 0; t <= {{positions|length}}; t++) {
-                               const timer = document.getElementById(t);
-                               const incrementTime = Math.round(timeTotal / {{positions|length}});
-
-                               let current = Math.round((Date.now() + data['timer_offset']) / 1000);
-                               var time = (data["end_timer_main"] - current) + (t * incrementTime);
-
-                               let negate = "";
-                               if (time > 0) {
-                                       var minutes = Math.floor(time / 60);
-                                       var seconds = (time - (minutes * 60));
-                               } else {
-                                       var minutes = Math.ceil(time / 60);
-                                       var seconds = -(time - (minutes * 60));
-                                       negate = "-";
-                               }
-                               if (minutes < 0) {seconds = 60 - seconds;} // reverse tick if in the past
-
-                               var minutesString = negate + minutes.toString().padStart(2, "0");
-                               var secondsString = seconds.toString().padStart(2, "0");
-
-                               timer.innerHTML = `T-${minutesString}:${secondsString}`.replace("--","+");
-                               if (minutes < 1) {
-                                       if (time <= 0) {timer.style.color = "red";}
-                                       else if (seconds % 2 == 0) {timer.style.color = "green";}
-                                       else {timer.style.color = "orange";}
-                               } else {
-                                       timer.style.color = "green";
-                               }
-
-                               if (t ==
-                                       {% for p in positions %}
-                                               {% if p.i == data.current_position %}
-                                                       {{loop.index}}
-                                               {% endif %}
-                                       {% endfor %}
-                                                       ) {
-                                       document.getElementById("next").innerHTML = `T-${minutesString}:${secondsString}`.replace("--","+");
-
-                                       const nextLabel = document.getElementById("nextLabel");
-                                       if (minutes < 1) {
-                                               if (time <= 0) {nextLabel.style.color = "red";}
-                                               else if (seconds % 2 == 0) {nextLabel.style.color = "green";}
-                                               else {nextLabel.style.color = "orange";}
-                                       } else {
-                                               nextLabel.style.color = "green";
-                                       }
-
-                                       var date = new Date(data["end_timer_main"] * 1000);
-                                       document.getElementById("start").innerHTML = `${date.getHours()}:${date.getMinutes()}`;
-                               }
+function update( data ) {
+       for (let t = 0; t <= {{positions|length}}; t++) {
+               const timer = document.getElementById(t);
+               const incrementTime = Math.round(timeTotal / {{positions|length}});
+
+               let current = Math.round((Date.now() + data['timer_offset']) / 1000);
+               var time = (data["end_timer_main"] - current) + (t * incrementTime);
+
+               let negate = "";
+               if (time > 0) {
+                       var minutes = Math.floor(time / 60);
+                       var seconds = (time - (minutes * 60));
+               } else {
+                       var minutes = Math.ceil(time / 60);
+                       var seconds = -(time - (minutes * 60));
+                       negate = "-";
+               }
+               if (minutes < 0) {seconds = 60 - seconds;} // reverse tick if in the past
+
+               var minutesString = negate + minutes.toString().padStart(2, "0");
+               var secondsString = seconds.toString().padStart(2, "0");
+
+               timer.innerHTML = `T-${minutesString}:${secondsString}`.replace("--","+");
+               if (minutes < 1) {
+                       if (time <= 0) {timer.style.color = "red";}
+                       else if (seconds % 2 == 0) {timer.style.color = "green";}
+                       else {timer.style.color = "orange";}
+               } else {
+                       timer.style.color = "green";
+               }
+
+               if (t ==
+                       {% for p in positions %}
+                               {% if p.i == data.current_position %}
+                                       {{loop.index}}
+                               {% endif %}
+                       {% endfor %}
+                                       ) {
+                       document.getElementById("next").innerHTML = `T-${minutesString}:${secondsString}`.replace("--","+");
+
+                       const nextLabel = document.getElementById("nextLabel");
+                       if (minutes < 1) {
+                               if (time <= 0) {nextLabel.style.color = "red";}
+                               else if (seconds % 2 == 0) {nextLabel.style.color = "green";}
+                               else {nextLabel.style.color = "orange";}
+                       } else {
+                               nextLabel.style.color = "green";
                        }
-               });
-}
 
-setInterval(update, 1000)
+                       var date = new Date(data["end_timer_main"] * 1000);
+                       document.getElementById("start").innerHTML = `${date.getHours()}:${date.getMinutes()}`;
+               }
+       }
+}
 
                </script>
        </body>
diff --git a/templates/display.html b/templates/display.html
deleted file mode 100644 (file)
index 57be682..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-       <head>
-               <meta charset="utf-8">
-               <title>XMDV Display</title>
-               <link rel="icon" type="image/x-icon" href="/static/assets/star3.svg">
-               <link rel="stylesheet" href="/static/display.css">
-       </head>
-       <body style="font-size:80px;">
-               <input type="range" min="30" max="100" value="80" id="slider">
-               <div id="numbers">
-                       T1: <span style="background-color: green;" id="timer_1">00:00</span> -
-                       T2: <span style="background-color: purple;" id="timer_2">00:00</span> -
-                       T3: <span style="background-color: blue;" id="timer_3">00:00</span><br>
-                       Left: <span id="left">0</span> out of <span id="stock">0</span><br>
-                       Start price: <span style="background-color:deeppink" id="price">0.00</span>
-                       &DownArrowBar;<span style="color:orange" id="gallery">00</span>%
-                       &#10504;<span style="color:red" id="cost">00</span>%<br>
-                       <span id="discountBox">Discount: <span style="background-color:darkviolet"><span id="discount">00</span>%</span> (<em>Now <span id="currentPrice" style="background-color:darkviolet;">0.00</span></em>)</span>
-               </div>
-               <script type="module">
-
-import { makeTime } from "../static/utils.js";
-import { io } from "https://cdn.socket.io/4.8.1/socket.io.esm.min.js";
-
-/*
-websocket updates when data is sent to the admin pannel, updating the autocue
-this eliminates the ovehead that comes from constantly polling the api.
-on connection, the system also sends the api data.
-*/
-
-var socket = io('/');
-socket.on('apiUpdate', function (data) {
-       console.info("Recived data, updating...");
-       update(data);
-});
-
-// update text when slider changes
-var slider = document.getElementById("slider");
-slider.oninput = function (e) {
-       document.getElementsByTagName("body")[0].style.fontSize = `${this.value}px`;
-}
-
-let rep;
-function update ( data ) {
-       // fetch the item manifest and chache it
-       fetch("./api/items", {cache: "default"})
-               .then(items => items.json())
-               .then(items => items.items)
-               .then(items => {
-                       let id = data.item_id;
-
-                       if (data.bool_number) { rep = "7"; }
-                       else { rep = "#####"; }
-
-                       // update the total items and the items sold already
-                       document.getElementById("left").innerHTML = String(Math.round(
-                               items[id].stock_count * (data.percent_remaining / 100)
-                               )).replaceAll(rep, "&#9608;");
-                       document.getElementById("stock").innerHTML = String(items[id].stock_count).replaceAll(rep, "&#9608;");
-
-                       // update the timers
-                       for (let t = 1; t <= 3; t++) {
-                               document.getElementById(`timer_${t}`).innerHTML = String(makeTime(
-                                       data[`end_timer_${t}`],
-                                       data['timer_offset'],
-                                       data[`bool_timer_${t}`]
-                                       )).replaceAll(rep, "&#9608;")
-                       }
-
-                       // update the discount
-                       document.getElementById("discount").innerHTML = String(data.discount).replaceAll(rep, "&#9608;");
-                       document.getElementById("gallery").innerHTML = String(items[id].gallery_price * 100).replaceAll(rep, "&#9608;");
-                       document.getElementById("cost").innerHTML = String(items[id].cost_price * 100).replaceAll(rep, "&#9608;");
-                       if (data.discount != 0) {
-                               document.getElementById("discountBox").classList.add("show");
-                       } else {
-                               document.getElementById("discountBox").classList.remove("show");
-                       }
-
-                       // update the prices
-                       const price = document.getElementById("price");
-                       const currentPrice = document.getElementById("currentPrice");
-                       if (items[id].prefix) {
-                               price.innerHTML = `${items[id].currency}${String(items[id].origional_price).replaceAll(rep, "&#9608;")}`;
-                               currentPrice.innerHTML = `${items[id].currency}${String(Math.round(items[id].origional_price * data.discount) / 100).replaceAll(rep, "&#9608;")}`;
-                       } else {
-                               price.innerHTML = `${String(items[id].origional_price).replaceAll(rep, "&#9608;")}${items[id].currency}`;
-                               currentPrice.innerHTML = `${String(Math.round(items[id].origional_price * data.discount) / 100).replaceAll(rep, "&#9608;")}${items[id].currency}`;
-                       }
-               });
-       }
-
-               </script>
-       </body>
-</html>
index 5878c5864350695b1be5c30c23266026b12b3bc3..cb23831e58ca59bd803a0563a04a17d8e4629ae0 100644 (file)
                <meta charset="utf-8">
                <title>XMDV Teleshopping</title>
                <link rel="icon" type="image/x-icon" href="/static/assets/star1.svg">
-               <style>
-
-body {
-    --magenta: purple;
-    --blue: violet;
-    --red: red;
-
-    font-family: "Archivo", sans-serif;
-    font-size: 1.6vh;
-    color: var(--black);
-
-    opacity: 0;
-    transition: opacity 1.5s;
-
-    overflow: hidden;
-}
-@keyframes spin {
-    0% {transform: rotate(0turn) scale(1.2);}
-    25% {transform: rotate(0.25turn) scale(1);}
-    50% {transform: rotate(0.5turn) scale(1.2);}
-    75% {transform: rotate(0.75turn) scale(1);}
-    100% {transform: rotate(1turn) scale(1.2);}
-}
-@keyframes spin1 {
-    0% {transform: perspective(100px) rotateY( -25deg ) rotateX(-25deg) rotateZ(0turn) scale(1.2);}
-    25% {transform: perspective(100px) rotateY( -25deg ) rotateX(-25deg) rotateZ(0.25turn) scale(1);}
-    50% {transform: perspective(100px) rotateY( -25deg ) rotateX(-25deg) rotateZ(0.5turn) scale(1.2);}
-    75% {transform: perspective(100px) rotateY( -25deg ) rotateX(-25deg) rotateZ(0.75turn) scale(1);}
-    100% {transform: perspective(100px) rotateY( -25deg ) rotateX(-25deg) rotateZ(1turn) scale(1.2);}
-}
-@keyframes spin2 {
-    0% {transform: perspective(100px) rotateY( 20deg ) rotateX(-10deg) rotateZ(0turn) scale(1.2);}
-    25% {transform: perspective(100px) rotateY( 20deg ) rotateX(-10deg) rotateZ(0.25turn) scale(1);}
-    50% {transform: perspective(100px) rotateY( 20deg ) rotateX(-10deg) rotateZ(0.5turn) scale(1.2);}
-    75% {transform: perspective(100px) rotateY( 20deg ) rotateX(-10deg) rotateZ(0.75turn) scale(1);}
-    100% {transform: perspective(100px) rotateY( 20deg ) rotateX(-10deg) rotateZ(1turn) scale(1.2);}
-}
-@keyframes spin3 {
-    0% {transform: perspective(100px) rotateY( -20deg ) rotateX(10deg) rotateZ(0turn) scale(1.2);}
-    25% {transform: perspective(100px) rotateY( -20deg ) rotateX(10deg) rotateZ(0.25turn) scale(1);}
-    50% {transform: perspective(100px) rotateY( -20deg ) rotateX(10deg) rotateZ(0.5turn) scale(1.2);}
-    75% {transform: perspective(100px) rotateY( -20deg ) rotateX(10deg) rotateZ(0.75turn) scale(1);}
-    100% {transform: perspective(100px) rotateY( -20deg ) rotateX(10deg) rotateZ(1turn) scale(1.2);}
-}
-@keyframes spin4 {
-    0% {transform: perspective(100px) rotateY( 25deg ) rotateX(25deg) rotateZ(0turn) scale(1.2);}
-    25% {transform: perspective(100px) rotateY( 25deg ) rotateX(25deg) rotateZ(0.25turn) scale(1);}
-    50% {transform: perspective(100px) rotateY( 25deg ) rotateX(25deg) rotateZ(0.5turn) scale(1.2);}
-    75% {transform: perspective(100px) rotateY( 25deg ) rotateX(25deg) rotateZ(0.75turn) scale(1);}
-    100% {transform: perspective(100px) rotateY( 25deg ) rotateX(25deg) rotateZ(1turn) scale(1.2);}
-}
-@keyframes spinOffset {
-    0% {transform: perspective(200px) translate(-50%, -50%) rotateY( 15deg ) rotateZ(0turn) scale(1.2);}
-    25% {transform: perspective(250px) translate(-50%, -50%) rotateY( 15deg ) rotateZ(0.25turn) scale(1);}
-    50% {transform: perspective(300px) translate(-50%, -50%) rotateY( 15deg ) rotateZ(0.5turn) scale(1.2);}
-    75% {transform: perspective(250px) translate(-50%, -50%) rotateY( 15deg ) rotateZ(0.75turn) scale(1);}
-    100% {transform: perspective(200px) translate(-50%, -50%) rotateY( 15deg ) rotateZ(1turn) scale(1.2);}
-}
-@keyframes spinText {
-    0% {transform: rotate(-0.01turn);}
-    50% {transform: rotate(0.01turn);}
-    100% {transform: rotate(-0.01turn);}
-}
-@keyframes spinText1 {
-    0% {transform: perspective(100px) rotateY( 20deg ) rotateX(-10deg) rotateZ(-0.01turn);}
-    50% {transform: perspective(100px) rotateY( 20deg ) rotateX(-10deg) rotateZ(0.01turn);}
-    100% {transform: perspective(100px) rotateY( 20deg ) rotateX(-10deg) rotateZ(-0.01turn);}
-}
-@keyframes spinText2 {
-    0% {transform: perspective(100px) rotateY( -20deg ) rotateZ(-0.01turn);}
-    50% {transform: perspective(100px) rotateY( -20deg ) rotateZ(0.01turn);}
-    100% {transform: perspective(100px) rotateY( -20deg ) rotateZ(-0.01turn);}
-}
-@keyframes spinText3 {
-    0% {transform: perspective(100px) rotateY( 20deg ) rotateZ(-0.01turn);}
-    50% {transform: perspective(100px) rotateY( 20deg ) rotateZ(0.01turn);}
-    100% {transform: perspective(100px) rotateY( 20deg ) rotateZ(-0.01turn);}
-}
-@keyframes spinText4 {
-    0% {transform: perspective(100px) rotateY( -20deg ) rotateX(10deg) rotateZ(-0.01turn);}
-    50% {transform: perspective(100px) rotateY( -20deg ) rotateX(10deg) rotateZ(0.01turn);}
-    100% {transform: perspective(100px) rotateY( -20deg ) rotateX(10deg) rotateZ(-0.01turn);}
-}
-@keyframes spinTextOffset {
-    0% {transform: perspective(500px) translateX(-50%) rotateY( -10deg ) rotateZ(-0.01turn);}
-    50% {transform: perspective(500px) translateX(-50%) rotateY( -10deg) rotateZ(0.01turn);}
-    100% {transform: perspective(500px) translateX(-50%) rotateY( -10deg ) rotateZ(-0.01turn);}
-}
-@keyframes flicker {
-    0% {
-        color: black;
-        text-shadow: -2px -2px 0 #FFF, 2px -2px 0 #FFF, -2px 2px 0 #FFF, 2px 2px 0 #FFF;
-    }
-    49.999% {
-        color: black;
-        text-shadow: -2px -2px 0 #FFF, 2px -2px 0 #FFF, -2px 2px 0 #FFF, 2px 2px 0 #FFF;
-    }
-    50% {
-        color: white;
-        text-shadow: -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000, 2px 2px 0 #000;
-    }
-    99.999% {
-        color: white;
-        text-shadow: -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000, 2px 2px 0 #000;
-    }
-    100% {
-        color: black;
-        text-shadow: -2px -2px 0 #FFF, 2px -2px 0 #FFF, -2px 2px 0 #FFF, 2px 2px 0 #FFF;
-    }
-}
+               <link rel="stylesheet" href="./static/fonts.css">
+               <link rel="stylesheet" href="./static/main.css">
+               <script type="module">
 
-.container {
-    position: absolute;
-}
-/*.box {
-    box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 3px, rgba(0, 0, 0, 0.24) 0px 1px 2px;
-    background-image: var(--background);
-}*/
-#side {
-    left: 6vh;
-    top: 6vh;
-    width: 40vh;
-    opacity: 0;
-    transition: opacity 1.5s;
-}
-.soldBox {
-    margin: 5px;
-    padding: 5px;
-    color: blue;
-    background-color: white;
-    box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 3px, rgba(0, 0, 0, 0.19) 0px 1px 2px;
-}
-.soldBox > * {
-    margin: 0;
-    font-style: italic;
-}
-.soldBox > * > span {
-    font-weight: bold;
-    font-style: normal;
-}
-#raiting {
-    margin: 5px;
-    margin-top: 0;
-    font-size: 2em;
-    line-height: 1em;
-    color: white;
-    background-color: var(--magenta);
-    box-shadow: pink 0px 1px 3px, pink 0px 1px 2px;
-}
-.main {
-    background-color: white;
-    z-index: 0;
-}
-#subtext {
-    color: white;
-    background-color: var(--blue);
-}
-#banner {
-    right: 6vh;
-    top: 6vh;
-    height: 2em;
-    width: 0vh;
-    padding: 0.5vh;
-    overflow: hidden;
-    opacity: 0;
-    transition: opacity 1.5s, width 1.5s, left 1.5s;
-    color: white;
-    background-color: var(--magenta);
-}
-#timer2.moved {
-    top: 6vh;
-}
-#timer3.focus {
-    bottom: calc(50% - 15vh);
-    right: calc(50% - 15vh);
-    width: 30vh;
-    height: 30vh;
-    border-radius: 15vh;
-    font-size: 5em;
-    line-height: 30vh;
-}
-.extra {
-    position: relative;
-    top: -2vh;
-    z-index: -1;
-    transform: translateY(-100%);
-    padding-top: 2vh;
-    padding-bottom: 1px;
-    color: white;
-    background-color: var(--magenta);
-    transition: transform 1.5s;
-}
-.showExtra > .extra {
-    transform: translateY(0%);
-}
-.main > *, .extra > *:not(.soldBox), .bottom > *, .bottom > div > *, #marquee > * {
-    margin: 0;
-    padding: 0.5vh 1vh;
-}
-#banner > h1 {
-    width: 49vh;
-    margin: 0;
-    font-size: 2em;
-    line-height: 1;
-    font-style: italic;
-    font-weight: bold;
-    text-align: center;
-}
-#currentPrice {
-    color: var(--red);
-    font-size: 2em;
-}
-#unitsLeft {
-    font-size: 3em;
-}
-#badge1Container {
-    position: relative;
-    padding: 0;
-    transform: translateX(12vh) scale(0%);
-    transition: transform 1.5s;
-}
-#badge1Container > img {
-    position: absolute;
-    top: -9vh;
-    left: 36vh;
-    width: 12vh;
-    height: 12vh;
-    padding: 0;
-    animation: spin1 12s linear 0s infinite;
-    animation-direction: reverse;
-}
-#badge1Text {
-    position: absolute;
-    top: -7.5vh;
-    left: 31.5vh;
-    color: yellow;
-    width: 22vh;
-    animation: spinText1 7s linear 0s infinite;
-    rotate: -0.015turn;
-    font-size: 2em;
-    text-align: center;
-    /*text-shadow: -2px -2px 0 green, 2px -2px 0 green, -2px 2px 0 green, 2px 2px 0 green;*/
-}
-#badge2Container {
-    position: relative;
-    padding: 0;
-    transform: translateX(12vh) scale(0%);
-    transition: transform 1.5s;
-}
-#badge2Container > img {
-    position: absolute;
-    top: -2vh;
-    left: 32vh;
-    width: 10vh;
-    height: 10vh;
-    padding: 0;
-    animation: spin2 10s linear 0s infinite;
-}
-#badge2Text {
-    position: absolute;
-    top: -1vh;
-    left: 26.5vh;
-    color: black;
-    width: 20vh;
-    animation: spinText2 10s linear 0s infinite;
-    rotate: 0.025turn;
-    text-align: center;
-    /*text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;*/
-}
-#badge3Container {
-    position: relative;
-    padding: 0;
-    transform: translateX(12vh) scale(0%);
-    transition: transform 1.5s;
-}
-#badge3Container > img {
-    position: absolute;
-    top: 3vh;
-    left: 38vh;
-    width: 10vh;
-    height: 10vh;
-    padding: 0;
-    animation: spin3 10s linear 0s infinite;
-    animation-direction: reverse;
-}
-#badge3Text {
-    position: absolute;
-    top: 4.9vh;
-    left: 33.9vh;
-    color: blue;
-    width: 20vh;
-    animation: spinText3 10s linear 0s infinite;
-    rotate: -0.03turn;
-    text-align: center;
-    /*text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;*/
-}
-#badge4Container {
-    position: relative;
-    padding: 0;
-    transform: translateX(12vh) scale(0%);
-    transition: transform 1.5s;
-}
-#badge4Container > img {
-    position: absolute;
-    top: 10vh;
-    left: 32vh;
-    width: 14vh;
-    height: 14vh;
-    padding: 0;
-    animation: spin4 10s linear 0s infinite;
-}
-#badge4Text {
-    position: absolute;
-    top: 11.8vh;
-    left: 28.4vh;
-    color: #00FF00;
-    width: 20vh;
-    animation: spinText4 10s linear 0s infinite;
-    rotate: 0.025turn;
-    font-size: 2.2em;
-    line-height: 0.6em;
-    text-align: center;
-    /*text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;*/
-}
-#shoutContainer {
-    position: absolute;
-    top: 50vh;
-    left: 50vw;
-    padding: 0;
-}
-#shoutBadgeContainer {
-    opacity: 0;
-    transform: translateX(-30vw);
-    transition: opacity 2s ease, transform 2s ease;
-}
-#shoutContainer.show > #shoutBadgeContainer {
-    opacity: 1;
-    transform: translateX(0vw);
-}
-#shoutTextContainer {
-    position: absolute;
-    top: -7vh;
-    opacity: 0;
-    transform: translateX(30vw);
-    transition: opacity 2s ease, transform 2s ease;
-    animation: flicker 1s linear 0s infinite;
-}
-#shoutContainer.show > #shoutTextContainer {
-    opacity: 1;
-    transform: translateX(0vw);
-}
-#shoutBadgeContainer > img {
-    width: 45vh;
-    height: 45vh;
-    padding: 0;
-    animation: spinOffset 10s linear 0s infinite;
-}
-#shout {
-    position: absolute;
-    width: 100vw;
-    top: 0;
-    perspective: 400px;
-    animation: spinTextOffset 10s linear 0s infinite;
-    rotate: 0.025turn;
-    font-size: 10em;
-    text-align: center;
-}
-#origionalPrice {
-    line-height: 0.5em;
-}
-#monthlyPrice {
-    line-height: 1em;
-}
-.main > hr, .extra > hr {
-    margin: 0 1vh;
-    padding: 0;
-    border-color: var(--dark);
-}
-.bottom {
-    left: 6vh;
-    bottom: 6vh;
-    width: 100vh;
-    background-color: white;
-}
-.bottom > *:not(#timer1) {
-    display: inline-block;
-    vertical-align: top;
-}
-#topTextBox {
-    width: calc(100% - 20px);
-    margin: 5px;
-    margin-bottom: 0;
-    padding: 5px;
-    color: white;
-    background-color: var(--blue);
-    box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 3px, rgba(0, 0, 0, 0.19) 0px 1px 2px;
-}
-#topText {
-    display: inline-block;
-    width: 50%;
-    padding: 0;
-}
-#idText {
-    display: inline-block;
-    width: 50%;
-    padding: 0;
-    text-align: right;
-    font-size: 1.5em;
-    font-weight: bold;
-    font-style: italic;
-    text-shadow: 0 0 3px rgba(0,0,0,0.25);
-}
-#marquee {
-    padding: 0;
-    width: 100%;
-    mask-image: linear-gradient(0.25turn, transparent, black 5%, black 95%, transparent);
-    color: var(--magenta);
-    overflow: hidden;
-}
-#marqueeText {
-    white-space: nowrap;
-    overflow: hidden;
-}
-#timer1 {
-    position: absolute;
-    left: 101vh;
-    padding: 0.5vh;
-    opacity: 0;
-    text-align: center;
-    color: white;
-    background-color: var(--magenta);
-    transition: opacity 1.5s;
-}
-#timer2 {
-    position: absolute;
-    right: calc(6vh + 2px);
-    top: calc(11.5vh + 2px);
-    height: 1.2em;
-    width: 3em;
-    margin: 0;
-    text-align: center;
-    color: black;
-    font-size: 2em;
-    font-weight: bold;
-    line-height: 1.2em;
-    opacity: 0;
-    transition: opacity 1.5s, left 1.5s, top 1.5s;
-}
-#timer3 {
-    position: absolute;
-    bottom: 4vh;
-    right: 4vh;
-    opacity: 0;
-    width: 12vh;
-    height: 12vh;
-    border-radius: 6vh;
-    margin: 0;
-    color: white;
-    text-align: center;
-    vertical-align: center;
-    font-family: "Calculator";
-    font-size: 2em;
-    font-weight: bold;
-    line-height: 12vh;
-    opacity: 0;
-    transition: opacity 1.5s, bottom 1.5s, right 1.5s, width 1.5s, height 1.5s, border-radius 1.5s, font-size 1.5s, line-height 1.5s, transform 1.5s;
-    animation: spinText 10s linear 0s infinite;
-}
-.show {
-    opacity: 1 !important;
-}
-#badge1Container.show, #badge2Container.show, #badge3Container.show, #badge4Container.show {
-    transform: translateX(0vh) scale(100%);
-}
-#banner.show {
-    width: 50vh;
-}
-#sigilBox {
-    opacity: 0;
-    mix-blend-mode: overlay;
-}
-#sigilBox.show {
-    opacity: 1;
-}
-.sigil {
-    position: fixed;
-    top: 50vh;
-    left: 50vw;
-    height: 90vh;
-    transform: translate(-50%, -50%);
-    opacity: 0;
-}
-.sigil.show {
-    opacity: 1;
-}
-@keyframes spinBox {
-    0% {transform: translateY(100%);}
-    50% {}
-    100% {}
+import { makeTime, getTime } from "../static/utils.js";
+import { io } from "https://cdn.socket.io/4.8.1/socket.io.esm.min.js";
 
-}
+/*
+websocket updates when data is sent to the admin pannel, updating the autocue
+this eliminates the ovehead that comes from constantly polling the api.
+on connection, the system also sends the api data.
+*/
+
+var socket = io('/');
+socket.on('apiUpdate', function (data) {
+       console.info("Recived data, updating...");
+       update(data);
+});
+
+socket.on('shoutUpdate', function (data) {
+       console.info("Recived shout, updating...");
+       shoutHandler(data);
+});
+
+function increment () {
+       for (let t = 1; t <= 3; t++) {
+               let timer = document.getElementById(`timer${t}`);
+               timer.innerHTML = makeTime(timer.dataset.end).replaceAll(rep, "&#9608;");
+
+               let [minutes, _, seconds] = getTime(timer.dataset.end);
+
+               // handle timer specific styling
+               if (t == 2) {
+
+                       let angle;
+                       if (minutes > 0) {angle = 360;}
+                       else {angle = seconds * (360 / 60)};
+                       timer2.style.background = `
+                       conic-gradient(
+                               rgb(0,0,0,0) 0deg,
+                                               rgb(0,0,0,0) ${angle}deg,
+                                               var(--blue) ${angle+.1}deg,
+                                               var(--blue) 360deg
+                       ), white`;
+
+               } else if (t == 3) {
+
+                       let radius;
+                       if (minutes > 0) {radius = 100;}
+                       else {radius = seconds * (100 / 60);}
+                       timer3.style.background = `
+                       linear-gradient(
+                               rgb(0,0,0,0) 0%, rgb(0,0,0,0) ${radius}%,
+                                                       purple ${radius+.1}%,
+                                                       purple 100%
+                       ), lightblue`;
+
+               }
+       }
+}
+
+setInterval(increment, 50);
 
-               </style>
-               <link rel="stylesheet" href="./static/fonts.css">
+               </script>
        </head>
-       <body id="all" class="show" onload="update();frame();">
+       <body id="all" class="show" onload="frame();">
 
                <canvas id="canvas" style="display: none;"></canvas>
 
@@ -555,7 +115,7 @@ body {
                        </div>
                        <div id="marquee">
                                <h1 class="marqueeText" id="bottomText"></h1>
-                       </div>  
+                       </div>
                </div>
 
                <div id="banner" class="container box">
@@ -564,38 +124,37 @@ body {
                <div id="timer2" class="box"></div>
                <div id="timer3"></div>
                <div id="shoutContainer">
-            <div id="shoutBadgeContainer">
-                <img class="badge" src="./static/assets/star5.svg"></img>
-            </div>
-            <div id="shoutTextContainer">
-                <h1 id="shout" style="top: 0">SHOUT TEXT</h1>
-            </div>
+                       <div id="shoutBadgeContainer">
+                               <img class="badge" src="./static/assets/star5.svg"></img>
+                       </div>
+                       <div id="shoutTextContainer">
+                               <h1 id="shout" style="top: 0">SHOUT TEXT</h1>
+                       </div>
                </div>
 
-        <div id="sigilBox">
-            {% for id in range(1, 19) %}
-                <object class="sigil" type="image/svg+xml" id="sigil{{ id }}" style="fill: green" data="/static/assets/sigil{{ id }}.svg"></object>
-            {% endfor %}
+               <div id="sigilBox">
+                       {% for id in range(1, 19) %}
+                               <object class="sigil" type="image/svg+xml" id="sigil{{ id }}" style="fill: green" data="/static/assets/sigil{{ id }}.svg"></object>
+                       {% endfor %}
                </div>
-
                <script>
 
 /*
- structure
+structure
 
- *: function
- -: section
+*: function
+-: section
 
- - setup of all const html elements
- - setup of all variables shared between the main call function and the other funtions
- * main call function (update) happens 2x per second
+- setup of all const html elements
+- setup of all variables shared between the main call function and the other funtions
+* main call function (update) happens 2x per second
 
- - setup of all variables remebered between frame calls
- * frame function (frame) 200x per second
+- setup of all variables remebered between frame calls
+* frame function (frame) 200x per second
 
- * sigil function (sigil) 2X per second
+* sigil function (sigil) 2X per second
 
- */
+*/
 
 const all = document.getElementById("all");
 
@@ -606,21 +165,21 @@ const timer4 = document.getElementById("timer4");
 const timer5 = document.getElementById("timer5");
 const timer6 = document.getElementById("timer6");
 const timers = [
-    timer1,
-timer2,
-timer3,
-timer4,
-timer5,
-timer6
+       timer1,
+       timer2,
+       timer3,
+       timer4,
+       timer5,
+       timer6
 ];
 const banner = document.getElementById("banner");
 const side = document.getElementById("side");
 const badgeContainer = [
-    document.getElementById("badge1Container"),
-    document.getElementById("badge2Container"),
-    document.getElementById("badge3Container"),
-    document.getElementById("badge4Container")
-    ]
+       document.getElementById("badge1Container"),
+       document.getElementById("badge2Container"),
+       document.getElementById("badge3Container"),
+       document.getElementById("badge4Container")
+       ]
 
 const sigilBox = document.getElementById("sigilBox");
 const remaining = document.getElementById("unitsLeft");
@@ -635,16 +194,16 @@ const marqueeContainer = document.getElementById("marquee");
 
 let discountRate = 0;
 let discountHard = [
-    0,
-    0,
-    0,
-    0
+       0,
+       0,
+       0,
+       0
 ]
 let discount = [
-    0,
-    0,
-    0,
-    0
+       0,
+       0,
+       0,
+       0
 ]
 
 let topText = "";
@@ -653,200 +212,150 @@ let bottomText = [];
 let rep = "#####";
 
 // handles all updates from the server data that are not required to be instantanious (2 times per second)
-function update() {
-    // fetch the item manifest and chache it
-    fetch("./api/items", {cache: "default"})
-    .then(data => data.json())
-    .then(dataStatic => {
-        fetch("./api", {cache: "no-store"})
-        .then(data => data.json())
-        .then(data => {
-
-            // some variable setup
-            let id = data.item_id;
-            const items = dataStatic.items
-            const item = items[id];
-
-            if (data.bool_number) { rep = "7"; }
-            else { rep = "#####"; }
-
-            // frame function variable hand-over
-            discountHard = [
-                1-(data.discount_1 / 100),
-                1-(data.discount_2 / 100),
-                1-(data.discount_3 / 100),
-                1-(data.discount_4 / 100)
-                ]
-            discountRate = data.discount_change / 200; // happens 200x per second in the frame function
-            topText = dataStatic.text.crawler_top[data.crawler_top_index]
-            bottomText = [];
-            for (let i of data.list_crawler_bottom) {bottomText.push(dataStatic.text.crawler_bottom[i])}
-            shout
-
-            // handle all optional elements showing/hiding
-            if (data.bool_all) {all.classList.add("show");}
-            else {all.classList.remove("show");}
-
-            // set all timers to correct time and show/focus
-            for (let t = 1; t <= 3; t++) {
-                const timer = timers[t-1];
-
-                if (timer != null) {
-                    let current = Math.round((Date.now() + data['timer_offset']) / 1000);
-                    var time = data[`end_timer_${t}`] - current;
-                    if (Math.sign(time) == -1) {time = 0;}
-                    var minutes = Math.floor(time / 60);
-                    var seconds = (time - (minutes * 60));
-
-                    var minutesString = minutes.toString().padStart(2, "0");
-                    var secondsString = seconds.toString().padStart(2, "0");
-
-                    timer.innerHTML = `${minutesString}:${secondsString}`.replaceAll(rep, "&#9608;");
-
-                    if (data[`bool_timer_${t}`]) {timer.classList.add("show");}
-                    else {timer.classList.remove("show");}
-
-                    if (data[`focus_timer_${t}`]) {timer.classList.add("focus");}
-                    else {timer.classList.remove("focus");}
-
-                    // additional styling for individual timers
-                    switch (t) {
-                        case 2: {
-                            let angle;
-                            if (minutes > 0) {angle = 360;}
-                            else {angle = Math.round(seconds * (360 / 60))};
-                            timer2.style.background = `
-                            conic-gradient(
-                                rgb(0,0,0,0) 0deg,
-                                           rgb(0,0,0,0) ${angle}deg,
-                                           var(--blue) ${angle}.1deg,
-                                           var(--blue) 360deg
-                            ), white`;
-                        }
-
-                        case 3: {
-                            let radius;
-                            if (minutes > 0) {radius = 100;}
-                            else {radius = Math.round(seconds * (100 / 60));}
-                            timer3.style.background = `
-                            linear-gradient(
-                                rgb(0,0,0,0) 0%, rgb(0,0,0,0) ${radius}%,
-                                            purple ${radius}.1%,
-                                            purple 100%
-                            ), lightblue`;
-                        }
-                    }
-                }
-            }
-
-            // banner placement
-            if (data.bool_banner) {
-                if (!banner.classList.contains("show")) { // only change the banner text when the banner is turning back on
-                    document.getElementById("bannerText").innerHTML = dataStatic.text.banner[data.banner_index].replaceAll(rep, "&#9608;");
-                }
-                banner.classList.add("show");
-                timer2.classList.remove("moved");
-            } else {
-                banner.classList.remove("show");
-                timer2.classList.add("moved");
-            }
-
-            // handle element showing
-            // hierarchy: main product information -> extra information -> discount badge
-            if (data.bool_product) {
-                side.classList.add("show");
-
-                if (data.bool_extra) {
-                    side.classList.add("showExtra");
-
-                    for (let d = 0; d < 4; d++) {
-                        if (discountHard[d] <= 0.99) {
-                            badgeContainer[d].classList.add("show");
-                        } else {
-                            badgeContainer[d].classList.remove("show");
-                        }
-                    }
-
-                } else {
-                    side.classList.remove("showExtra");
-                    for (let d = 0; d < 4; d++) {
-                        badgeContainer[d].classList.remove("show");
-                        }
-                }
-
-            } else {
-                side.classList.remove("show");
-                side.classList.remove("showExtra");
-                badgeContainer.classList.remove("show");
-            }
-
-            if (data.bool_sigil) { sigilBox.classList.add("show"); }
-            else { sigilBox.classList.remove("show"); }
-
-            // Shout
-            shout.innerHTML = data.shout.replaceAll(rep, "&#9608;");
-            let current = Math.round((Date.now() + data['timer_offset']) / 1000);
-            var time = data[`end_timer_shout`] - current;
-            if (time > 0) {
-                document.getElementById("shoutContainer").classList.add("show");
-            } else {
-                document.getElementById("shoutContainer").classList.remove("show");
-            }
-
-            // set item properties
-            document.getElementById("code").innerHTML = item.code.replaceAll(rep, "&#9608;");
-            document.getElementById("raiting").innerHTML = item.rating.replaceAll(rep, "&#9608;");
-            document.getElementById("subtext").innerHTML = item.subtext.replaceAll(rep, "&#9608;");
-            document.getElementById("description").innerHTML = item.description.replaceAll(rep, "&#9608;");
-
-            // calculate the price sting with the pre/postfix
-            let price = item.origional_price * discountHard.reduce((a, b)=> a*b, 1);
-            if (data.bool_rounding) {
-                price = Math.round(price * 100) / 100;
-            }
-
-            let priceString;
-            let ezString;
-            if (item.prefix) {priceString = `${item.currency}${price}`}
-            else {priceString = `${price}${item.currency}`};
-            let ezPrice = Math.round((price * 1.1) / 12);
-            if (item.prefix) {ezString = `${item.currency}${ezPrice}`}
-            else {ezString = `${ezPrice}${item.currency}`};
-
-            // set discount, pricing and ez pay
-            document.getElementById("currentPrice").innerHTML = `<em>Now only:</em> ${priceString}`.replaceAll(rep, "&#9608;");
-            document.getElementById("monthlyPrice").innerHTML = `12 monthly payments of <b>${ezString}</b>`.replaceAll(rep, "&#9608;");
-            document.getElementById("badge1Text").innerHTML = `${Math.round((1 - discountHard[0]) * 100)}% OFF`.replaceAll(rep, "&#9608;");
-            document.getElementById("badge2Text").innerHTML = `ONLY ${Math.round((discountHard[1]) * 100)}%`.replaceAll(rep, "&#9608;");
-            document.getElementById("badge3Text").innerHTML = `${Math.round((1 - discountHard[2]) * 100)}% REMOVED`.replaceAll(rep, "&#9608;");
-            document.getElementById("badge4Text").innerHTML = `10 FOR ${String(Math.round(discountHard[3] * 10)).replaceAll(rep, "&#9608;")}<br><span style='font-size: 0.7em;'>WHAT?</span>`;
-            document.getElementById("stock").innerHTML = `${item.stock_count} units`.replaceAll(rep, "&#9608;");
-            document.getElementById("sold").innerHTML = `${Math.round(item.stock_count * (1 - (data.percent_remaining / 100)))} units`.replaceAll(rep, "&#9608;");
-
-            if (discountHard.reduce((a, b)=> a*b, 1) <= 0.99) {
-                let origionalPrice = Math.round(item.origional_price);
-                if (item.prefix) {origionalString = `${item.currency}${origionalPrice}`}
-                else {origionalString = `${origionalPrice}${item.currency}`};
-                origional.innerHTML = `<s><em>WAS:</em> ${origionalString}</s>`.replaceAll(rep, "&#9608;");
-            } else {
-                origional.innerHTML = "<em>Amazing offer!<em>";
-            }
-
-            if (data.percent_remaining == 0) {
-                remaining.innerHTML = "Sold out!";
-            }
-            else { // work out the prefix for the discount to be more reactive
-                let descriptior;
-                if (data.percent_remaining < 0.1) {descriptor = "Quick! Only"}
-                else if (data.percent_remaining < 0.25) {descriptor = "Only"}
-                else if (data.percent_remaining < 0.5) {descriptor = "Just"}
-                else {descriptor = ""}
-                remaining.innerHTML = `
-                ${descriptor} <span style='font-family: "Calculator"; font-size: 0.75em;'>${String(Math.round(item.stock_count * (data.percent_remaining / 100))).replaceAll(rep, "&#9608;")}</span> left!
-                `; }
-        });
-    });
-}
+function update ( data ) {
+       // fetch the item manifest and chache it
+       fetch("./api/items", {cache: "default"})
+       .then(dataStatic => dataStatic.json())
+       .then(dataStatic => {
+
+                       // some variable setup
+                       let id = data.item_id;
+                       const items = dataStatic.items
+                       const item = items[id];
+
+                       if (data.bool_number) { rep = "7"; }
+                       else { rep = "#####"; }
+
+                       // frame function variable hand-over
+                       discountHard = [
+                               1-(data.discount_1 / 100),
+                               1-(data.discount_2 / 100),
+                               1-(data.discount_3 / 100),
+                               1-(data.discount_4 / 100)
+                               ]
+                       discountRate = data.discount_change / 200; // happens 200x per second in the frame function
+                       topText = dataStatic.text.crawler_top[data.crawler_top_index]
+                       bottomText = [];
+                       for (let i of data.list_crawler_bottom) {bottomText.push(dataStatic.text.crawler_bottom[i])}
+                       shout
+
+                       // handle all optional elements showing/hiding
+                       if (data.bool_all) {all.classList.add("show");}
+                       else {all.classList.remove("show");}
+
+                       // set all timers to correct time and show/focus
+                       for (let t = 1; t <= 3; t++) {
+                               const timer = timers[t-1];
+
+                               if (timer != null) {
+                                       timer.dataset.end = data[`end_timer_${t}`] + (data['timer_offset'] / 1000);
+
+                                       if (data[`bool_timer_${t}`]) {timer.classList.add("show");}
+                                       else {timer.classList.remove("show");}
+
+                                       if (data[`focus_timer_${t}`]) {timer.classList.add("focus");}
+                                       else {timer.classList.remove("focus");}
+
+                               } else {console.error("FUCK MAN OH FUCK!")}
+                       }
+
+                       // banner placement
+                       if (data.bool_banner) {
+                               if (!banner.classList.contains("show")) { // only change the banner text when the banner is turning back on
+                                       document.getElementById("bannerText").innerHTML = dataStatic.text.banner[data.banner_index].replaceAll(rep, "&#9608;");
+                               }
+                               banner.classList.add("show");
+                               timer2.classList.remove("moved");
+                       } else {
+                               banner.classList.remove("show");
+                               timer2.classList.add("moved");
+                       }
+
+                       // handle element showing
+                       // hierarchy: main product information -> extra information -> discount badge
+                       if (data.bool_product) {
+                               side.classList.add("show");
+
+                               if (data.bool_extra) {
+                                       side.classList.add("showExtra");
+
+                                       for (let d = 0; d < 4; d++) {
+                                               if (discountHard[d] <= 0.99) {
+                                                       badgeContainer[d].classList.add("show");
+                                               } else {
+                                                       badgeContainer[d].classList.remove("show");
+                                               }
+                                       }
+
+                               } else {
+                                       side.classList.remove("showExtra");
+                                       for (let d = 0; d < 4; d++) {
+                                               badgeContainer[d].classList.remove("show");
+                                               }
+                               }
+
+                       } else {
+                               side.classList.remove("show");
+                               side.classList.remove("showExtra");
+                               badgeContainer.classList.remove("show");
+                       }
+
+                       if (data.bool_sigil) { sigilBox.classList.add("show"); }
+                       else { sigilBox.classList.remove("show"); }
+
+                       // set item properties
+                       document.getElementById("code").innerHTML = item.code.replaceAll(rep, "&#9608;");
+                       document.getElementById("raiting").innerHTML = item.rating.replaceAll(rep, "&#9608;");
+                       document.getElementById("subtext").innerHTML = item.subtext.replaceAll(rep, "&#9608;");
+                       document.getElementById("description").innerHTML = item.description.replaceAll(rep, "&#9608;");
+
+                       // calculate the price sting with the pre/postfix
+                       let price = item.origional_price * discountHard.reduce((a, b)=> a*b, 1);
+                       if (data.bool_rounding) {
+                               price = Math.round(price * 100) / 100;
+                       }
+
+                       let priceString;
+                       let ezString;
+                       if (item.prefix) {priceString = `${item.currency}${price}`}
+                       else {priceString = `${price}${item.currency}`};
+                       let ezPrice = Math.round((price * 1.1) / 12);
+                       if (item.prefix) {ezString = `${item.currency}${ezPrice}`}
+                       else {ezString = `${ezPrice}${item.currency}`};
+
+                       // set discount, pricing and ez pay
+                       document.getElementById("currentPrice").innerHTML = `<em>Now only:</em> ${priceString}`.replaceAll(rep, "&#9608;");
+                       document.getElementById("monthlyPrice").innerHTML = `12 monthly payments of <b>${ezString}</b>`.replaceAll(rep, "&#9608;");
+                       document.getElementById("badge1Text").innerHTML = `${Math.round((1 - discountHard[0]) * 100)}% OFF`.replaceAll(rep, "&#9608;");
+                       document.getElementById("badge2Text").innerHTML = `ONLY ${Math.round((discountHard[1]) * 100)}%`.replaceAll(rep, "&#9608;");
+                       document.getElementById("badge3Text").innerHTML = `${Math.round((1 - discountHard[2]) * 100)}% REMOVED`.replaceAll(rep, "&#9608;");
+                       document.getElementById("badge4Text").innerHTML = `10 FOR ${String(Math.round(discountHard[3] * 10)).replaceAll(rep, "&#9608;")}<br><span style='font-size: 0.7em;'>WHAT?</span>`;
+                       document.getElementById("stock").innerHTML = `${item.stock_count} units`.replaceAll(rep, "&#9608;");
+                       document.getElementById("sold").innerHTML = `${Math.round(item.stock_count * (1 - (data.percent_remaining / 100)))} units`.replaceAll(rep, "&#9608;");
+
+                       if (discountHard.reduce((a, b)=> a*b, 1) <= 0.99) {
+                               let origionalPrice = Math.round(item.origional_price);
+                               if (item.prefix) {origionalString = `${item.currency}${origionalPrice}`}
+                               else {origionalString = `${origionalPrice}${item.currency}`};
+                               origional.innerHTML = `<s><em>WAS:</em> ${origionalString}</s>`.replaceAll(rep, "&#9608;");
+                       } else {
+                               origional.innerHTML = "<em>Amazing offer!<em>";
+                       }
+
+                       if (data.percent_remaining == 0) {
+                               remaining.innerHTML = "Sold out!";
+                       }
+                       else { // work out the prefix for the discount to be more reactive
+                               let descriptior;
+                               if (data.percent_remaining < 0.1) {descriptor = "Quick! Only"}
+                               else if (data.percent_remaining < 0.25) {descriptor = "Only"}
+                               else if (data.percent_remaining < 0.5) {descriptor = "Just"}
+                               else {descriptor = ""}
+                               remaining.innerHTML = `
+                               ${descriptor} <span style='font-family: "Calculator"; font-size: 0.75em;'>${String(Math.round(item.stock_count * (data.percent_remaining / 100))).replaceAll(rep, "&#9608;")}</span> left!
+                               `; }
+               });
+       }
 
 // frame dynamic variables
 
@@ -857,57 +366,64 @@ let bottomTextWidth = 0;
 // function handles all animated events that are required to look smooth (marquee movement / price changes) (200 times per second)
 function frame() {
 
-    // move the current discount towards the target distance at the supplied rate
-    for (let d = 0; d < 4; d++) {
-        if (Math.abs(discount[d] - discountHard[d]) <= discountRate) {discount[d] = discountHard[d];}
-        else if (discount[d] > discountHard[d]) {discount[d] -= discountRate;}
-        else {discount[d] += discountRate;}
-    }
+       // move the current discount towards the target distance at the supplied rate
+       for (let d = 0; d < 4; d++) {
+               if (Math.abs(discount[d] - discountHard[d]) <= discountRate) {discount[d] = discountHard[d];}
+               else if (discount[d] > discountHard[d]) {discount[d] -= discountRate;}
+               else {discount[d] += discountRate;}
+       }
 
-    bottomTextElement.style.transform = `translateX(${marqueeOffset}px)`;
-    marqueeOffset -= 0.2;
+       bottomTextElement.style.transform = `translateX(${marqueeOffset}px)`;
+       marqueeOffset -= 0.2;
 
-    // only replace text where neccicary or where the page has just loaded
-    if (marqueeOffset < -bottomTextWidth || (topTextElement.innerHTML == "" && bottomTextElement.innerHTML == "")) {
-        if (bottomText.length == 0) {return}
+       // only replace text where neccicary or where the page has just loaded
+       if (marqueeOffset < -bottomTextWidth || (topTextElement.innerHTML == "" && bottomTextElement.innerHTML == "")) {
+               if (bottomText.length == 0) {return}
 
-        topTextElement.innerHTML = topText.replaceAll(rep, "&#9608;");
+               topTextElement.innerHTML = topText.replaceAll(rep, "&#9608;");
 
-        bottomTextElement.innerHTML = String(bottomText[bottomTextIndex]).replaceAll(rep, "&#9608;");
-        bottomTextIndex += 1;
-        if (bottomTextIndex >= bottomText.length) { // make sure the changed list wont ovreflow the bottom text index
-            bottomTextIndex = 0;
-        }
+               bottomTextElement.innerHTML = String(bottomText[bottomTextIndex]).replaceAll(rep, "&#9608;");
+               bottomTextIndex += 1;
+               if (bottomTextIndex >= bottomText.length) { // make sure the changed list wont ovreflow the bottom text index
+                       bottomTextIndex = 0;
+               }
 
-        // use a canvas element to measure the size of text precicely
-        const canvas = document.getElementById("canvas");
-        const ctx = canvas.getContext("2d");
+               // use a canvas element to measure the size of text precicely
+               const canvas = document.getElementById("canvas");
+               const ctx = canvas.getContext("2d");
 
-        var style = window.getComputedStyle(bottomTextElement, null).getPropertyValue("font-size");
-        var fontSize = parseFloat(style);
+               var style = window.getComputedStyle(bottomTextElement, null).getPropertyValue("font-size");
+               var fontSize = parseFloat(style);
 
-        ctx.font = `bold ${fontSize}px sans-serif`;
-        let text = ctx.measureText(bottomTextElement.innerHTML);
+               ctx.font = `bold ${fontSize}px sans-serif`;
+               let text = ctx.measureText(bottomTextElement.innerHTML);
 
-        bottomTextWidth = text.width;
+               bottomTextWidth = text.width;
 
-        marqueeOffset = window.innerHeight ; // place text just before its end-point
-    }
+               marqueeOffset = window.innerHeight ; // place text just before its end-point
+       }
 }
 
 let sigilCounter = 1;
 function sigil () {
-    document.getElementById(`sigil${sigilCounter}`).classList.remove("show");
+       document.getElementById(`sigil${sigilCounter}`).classList.remove("show");
 
-    sigilCounter ++;
-    if (sigilCounter > 18) { sigilCounter = 1; }
+       sigilCounter ++;
+       if (sigilCounter > 18) { sigilCounter = 1; }
 
-    document.getElementById(`sigil${sigilCounter}`).classList.add("show");
+       document.getElementById(`sigil${sigilCounter}`).classList.add("show");
 }
 
+function shoutHandler( data ) {
+       shout.innerHTML = data.text.replaceAll(rep, "&#9608;");
+       document.getElementById("shoutContainer").classList.add("show");
+
+       setTimeout( function() {
+               document.getElementById("shoutContainer").classList.remove("show");
+       }, data.time * 1000);
+}
 
 setInterval(sigil, 100);
-setInterval(update, 500);
 setInterval(frame, 5);
 
                </script>
diff --git a/templates/hud.html b/templates/hud.html
new file mode 100644 (file)
index 0000000..56c0572
--- /dev/null
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<html lang="en">
+       <head>
+               <meta charset="utf-8">
+               <title>XMDV Display</title>
+               <link rel="icon" type="image/x-icon" href="/static/assets/star3.svg">
+               <link rel="stylesheet" href="/static/display.css">
+       </head>
+       <body style="font-size:80px;">
+               <input type="range" min="30" max="100" value="80" id="slider">
+               <div id="numbers">
+                       T1: <span data-end="0" id="timer_1">00:00</span> -
+                       T2: <span data-end="0" id="timer_2">00:00</span> -
+                       T3: <span data-end="0" id="timer_3">00:00</span><br>
+                       Left:
+                       <span id="left">0</span> out of
+                       <span id="stock">0</span><br>
+                       Start price:
+                       <span style="background-color:deeppink" id="price">0.00</span>
+                       &DownArrowBar;<span style="color:orange" id="gallery">00</span>%
+                       &#10504;<span style="color:red" id="cost">00</span>%<br>
+                       <span id="discountBox">Discount:
+                               <span style="background-color:darkviolet">
+                                       <span id="discount">00</span>%
+                               </span>
+                               (<em>Now
+                                       <span id="currentPrice" style="background-color:darkviolet;">0.00</span>
+                               </em>)
+                       </span>
+               </div>
+               <script type="module">
+
+import { makeTime } from "../static/utils.js";
+import { io } from "https://cdn.socket.io/4.8.1/socket.io.esm.min.js";
+
+/*
+websocket updates when data is sent to the admin pannel, updating the autocue
+this eliminates the ovehead that comes from constantly polling the api.
+on connection, the system also sends the api data.
+*/
+
+var socket = io('/');
+socket.on('apiUpdate', function (data) {
+       console.info("Recived data, updating...");
+       update(data);
+});
+
+// update text when slider changes
+var slider = document.getElementById("slider");
+slider.oninput = function (e) {
+       document.getElementsByTagName("body")[0].style.fontSize = `${this.value}px`;
+}
+
+function increment() {
+       for (let t = 1; t <= 3; t++) {
+               let timer = document.getElementById(`timer_${t}`);
+               timer.innerHTML = makeTime(timer.dataset.end);
+       }
+}
+
+let rep;
+function update ( data ) {
+       // fetch the item manifest and chache it
+       fetch("./api/items", {cache: "default"})
+               .then(items => items.json())
+               .then(items => items.items)
+               .then(items => {
+                       let id = data.item_id;
+
+                       if (data.bool_number) { rep = "7"; }
+                       else { rep = "#####"; }
+
+                       // update the total items and the items sold already
+                       document.getElementById("left").innerHTML = String(Math.round(
+                               items[id].stock_count * (data.percent_remaining / 100)
+                               )).replaceAll(rep, "&#9608;");
+                       document.getElementById("stock").innerHTML = String(items[id].stock_count).replaceAll(rep, "&#9608;");
+
+                       for (let t = 1; t <= 3; t++) {
+                               // update the timer styling
+                               let timer = document.getElementById(`timer_${t}`);
+                               if (!data[`bool_timer_${t}`]) {
+                                       timer.style = "text-decoration: line-through;";
+                               } else {
+                                       if (data[`focus_timer_${t}`]) {
+                                               timer.style = "text-decoration: underline;";
+                                       } else {
+                                               timer.style = "";
+                                       }
+                               }
+                               timer.innerHTML = timer.innerHTML.replaceAll(rep, "&#9608;")
+
+                               // update the timer data
+                               timer.dataset.end = data[`end_timer_${t}`] + (data['timer_offset'] / 1000);
+                       }
+
+                       // update the discount
+                       document.getElementById("discount").innerHTML = String(data.discount).replaceAll(rep, "&#9608;");
+                       document.getElementById("gallery").innerHTML = String(items[id].gallery_price * 100).replaceAll(rep, "&#9608;");
+                       document.getElementById("cost").innerHTML = String(items[id].cost_price * 100).replaceAll(rep, "&#9608;");
+                       if (data.discount != 0) {
+                               document.getElementById("discountBox").classList.add("show");
+                       } else {
+                               document.getElementById("discountBox").classList.remove("show");
+                       }
+
+                       // update the prices
+                       const price = document.getElementById("price");
+                       const currentPrice = document.getElementById("currentPrice");
+                       if (items[id].prefix) {
+                               price.innerHTML = `${items[id].currency}${String(items[id].origional_price).replaceAll(rep, "&#9608;")}`;
+                               currentPrice.innerHTML = `${items[id].currency}${String(Math.round(items[id].origional_price * (1 - data.discount / 100)) ).replaceAll(rep, "&#9608;")}`;
+                       } else {
+                               price.innerHTML = `${String(items[id].origional_price).replaceAll(rep, "&#9608;")}${items[id].currency}`;
+                               currentPrice.innerHTML = `${String(Math.round(items[id].origional_price * (1 - data.discount / 100)) ).replaceAll(rep, "&#9608;")}${items[id].currency}`;
+                       }
+               });
+       }
+
+setInterval(increment, 200);
+
+               </script>
+       </body>
+</html>
index 802413b7dc4e9eaa2a1d5b686a1227485c6842d8..528a1fff45649dbdfd1ce71b09e63184828141a3 100644 (file)
 
                                <input type='number' name='discount' step='1' value='{{data.discount}}' id="discount">
                                <label>Discount</label> <label style='color: red;'>was {{data.discount}}</label><br>
-                               <em>Individual discounts (automatic)</em><br>
-                               {% for d in (1,2,3,4) %}
-                                       <label>Discount {{d}}</label>
-                                       <input type='text' name='discount_{{d}}_display' value='{{data["discount_" ~ d]}}' disabled>
-                                       <br>
-                               {% endfor %}
+                               <div style="display: none">
+                                       <em>Individual discounts (automatic)</em><br>
+                                       {% for d in (1,2,3,4) %}
+                                               <label>Discount {{d}}</label>
+                                               <input type='text' name='discount_{{d}}_display' value='{{data["discount_" ~ d]}}' disabled>
+                                               <br>
+                                       {% endfor %}
+                               </div>
 
                                <br>
 
index 0bf3b6f57cd07f7037b97448fa4d58f1b5309e04..e43280d3980d5d21edfa8200faf55b883e3f1f3a 100644 (file)
@@ -5,14 +5,14 @@
                <title>XMDV Admin - Timer</title>
                <link rel="icon" type="image/x-icon" href="/static/assets/star2.svg">
        </head>
-       <body onload="update();">
+       <body>
                <a href="/admin">.. Back to admin pannel</a>
                <form action="/admin/timer" method="POST">
                        <fieldset>
                                <legend>Timers</legend>
                                <p style='color: red;'>Note: time is approximate and may not be accurate to the GFX display.</p>
                                {% for t in (1,2,3) %}
-                                       <label>Timer {{t}}</label> <label style='color: blue;' id='{{t}}'>00:00</label>
+                                       <label>Timer {{t}}</label> <label data-end='0' style='color: blue;' id='{{t}}'>00:00</label>
                                        <input type='radio' value='0' name='end_timer_{{t}}'><label>Reset</label>
                                        <input type='radio' value='30' name='end_timer_{{t}}'><label>+00:30</label>
                                        <input type='radio' value='60' name='end_timer_{{t}}'><label>+01:00</label>
 
                <br>
 
-               <form action="/admin/timer" method="POST">
-                       <fieldset>
-                               <legend>Shout text</legend>
-                               <textarea name='shout' rows='2' style='width: 100%;'></textarea>
-                               <label>Time to live </label>
-                               <input type="number" value="5" name="end_timer_shout"></input>
-                               <label> seconds (this defaults to 5s)</label>
-                       </fieldset>
-                       <input type="submit" name="update" value="Shout">
-               </form>
-               <script>
+               <fieldset>
+                       <legend>Shout text</legend>
+                       <input type="text" id='shoutText' style='width: 100%;' />
+                       <label>Time to live </label>
+                       <input type="number" value="5" id="shoutTime"></input>
+                       <label> seconds (this defaults to 5s)</label>
+                       <button id="shoutUpdate">Shout</button>
+               </fieldset>
+               <script type="module">
+
+import { makeTime } from "../static/utils.js";
+import { io } from "https://cdn.socket.io/4.8.1/socket.io.esm.min.js";
 
-function update() {
-       fetch("/api", {cache: "no-store"})
-               .then(data => data.json())
-               .then(data => {
-                       for (let t = 1; t <= 6; t++) {
-                               const timer = document.getElementById(t);
+/*
+websocket updates when data is sent to the admin pannel, updating the autocue
+this eliminates the ovehead that comes from constantly polling the api.
+on connection, the system also sends the api data.
+*/
 
-                               let current = Math.round((Date.now() + data['timer_offset']) / 1000);
-                               var time = data[`end_timer_${t}`] - current;
-                               if (Math.sign(time) == -1) {time = 0;}
-                               var minutes = Math.floor(time / 60);
-                               var seconds = (time - (minutes * 60));
+var socket = io('/');
+socket.on('apiUpdate', function (data) {
+       console.info("Recived data, updating...");
+       update(data);
+});
 
-                               var minutesString = minutes.toString().padStart(2, "0");
-                               var secondsString = seconds.toString().padStart(2, "0");
+function increment() {
+       for (let t = 1; t <= 3; t++) {
+               let timer = document.getElementById(t);
+               timer.innerHTML = makeTime(timer.dataset.end);
+       }
+}
 
-                               timer.innerHTML = `${minutesString}:${secondsString}`;
-                               if (minutes == 0) {
-                                       if (seconds == 0) {timer.style.color = "blue";}
-                                       else if (seconds % 2 == 0) {timer.style.color = "red";}
-                                       else {timer.style.color = "orange";}
-                               } else {
-                                       timer.style.color = "green";
-                               }
-                       }
-               });
+function update ( data ) {
+       for (let t = 1; t <= 3; t++) {
+               // update the timer data
+               let timer = document.getElementById(t);
+               timer.dataset.end = data[`end_timer_${t}`] + (data['timer_offset'] / 1000);
+       }
 }
 
-setInterval(update, 1000)
+function shoutUpdate () {
+       let shoutTime = parseInt(document.getElementById("shoutTime").value);
+       let shoutText = document.getElementById("shoutText").value;
+       socket.emit("shoutUpdate", { time: shoutTime, text: shoutText });
+       console.log("Sent shout");
+       document.getElementById("shoutText").value = "";
+}
+
+document.getElementById("shoutUpdate").addEventListener("click", shoutUpdate, false);
+
+
+const input = document.getElementById("shoutText")
+       .addEventListener("keypress", function(event) {
+               if (event.key === "Enter") {
+                       event.preventDefault();
+                       shoutUpdate();
+               }
+       });
+
+setInterval(increment, 200);
 
                </script>
        </body>