main : .venv/touchfile
npx tsc
- .venv/bin/flask --app director run --debug
+ .venv/bin/flask --app argo run --debug
.venv/touchfile : requirements.txt
python3 -m venv .venv
--- /dev/null
+from werkzeug.security import generate_password_hash, check_password_hash
+from flask import Flask, render_template, send_from_directory, request
+from flask_socketio import SocketIO, emit
+from flask_httpauth import HTTPBasicAuth
+from flask_cors import CORS
+import json
+import os
+
+from .utils import deindex
+
+STAR = os.getenv("DELTA_VELORUM", "B")
+import argo.admin as admin
+
+from argo.database.read import read_views as database_read_views
+
+app = Flask(__name__, instance_relative_config=False)
+socketio = SocketIO(app, logger=False, engineio_logger=False)
+auth = HTTPBasicAuth()
+CORS(app)
+
+# import authentication data
+
+with open(f"{app.root_path}/../../secrets.json", "r", encoding="utf-8") as f:
+ users = json.loads(f.read())
+users = {k: generate_password_hash(v) for (k, v) in users.items()}
+
+# password verifier
+
+def verify_password(username, password):
+ if username in users and check_password_hash(users.get(username), password):
+ return username
+
+auth.verify_password(verify_password)
+
+# root movements
+
+@app.route("/", methods=['get'])
+def index():
+ return render_template("index.html")
+
+@socketio.on('connect')
+def connecting():
+ emit("update", database_read_views(), json=True, namespace="/", broadcast=True)
+
+# info screen pages
+
+@app.route("/info/<string:screen>", methods=["get"])
+def info(screen):
+ return render_template(f"info/{screen}.html")
+
+# admin screen pages
+
+app.route("/admin", methods=["get"])(auth.login_required(admin.admin_main))
+app.route("/admin/<string:table_name>", methods=["get", "post"])(auth.login_required(admin.admin_table))
+
+# script pages
+
+@app.route("/script/<path:filename>", methods=["get"])
+def script(filename):
+ return send_from_directory(os.path.join(app.root_path, "build"), filename)
+
+@app.route("/db", methods=["get"])
+@auth.login_required
+def download_db():
+ return send_from_directory(f"{app.root_path}/../data", "main.db")
--- /dev/null
+from flask import render_template, request
+from flask_socketio import emit
+import time
+
+from argo.database.read import read_indexes as database_read_indexes
+from argo.database.read import read_views as database_read_views
+from argo.database.write import write as database_write
+from argo.database.read import read as database_read
+
+from argo import STAR
+
+def admin_main():
+ return render_template(
+ "admin.html",
+ indexes=database_read_indexes(),
+ star=STAR
+ )
+
+def admin_table(table_name):
+ start = time.time()
+
+ if request.method == "POST":
+ database_write(request.form)
+
+ emit(
+ "update",
+ database_read_views(table_name),
+ json=True, namespace="/", broadcast=True
+ )
+
+ end = time.time()
+
+ return render_template(
+ "table.html",
+ indexes=database_read_indexes(),
+ table=database_read(table_name),
+ table_name=table_name,
+ time_taken = end - start,
+ star=STAR
+ )
--- /dev/null
+"""
+ package to read and write to the database
+
+ check if file exists and pragam check the database on init
+"""
+
+from .utils import integrity_check, optimize
+import sqlite3
+import os
+
+assert os.path.exists("./data/main.db"), "Database missing"
+assert integrity_check(), "Database integrity error"
+
+optimize()
--- /dev/null
+import sqlite3
+
+from .utils import list_all, list_views
+
+def read_all_current():
+ tables = list_all()
+ indexes = read_indexes()
+
+ with sqlite3.connect("./data/main.db") as connection:
+ cursor = connection.cursor()
+
+ data = {}
+ for table in tables:
+ if table == "indexes": continue
+
+ if "__view" in table:
+ index_table = table.split("__view")[0]
+ else: index_table = table
+
+ cursor.execute(f"SELECT * FROM {table};")
+
+ names = list(map(lambda x: f"{table}~~{x[0]}", cursor.description))
+ row = cursor.fetchall()[indexes[f"int--{index_table}"]]
+
+ data.update(dict(zip(names, row)))
+
+ return data
+
+def read(table):
+ """
+ reads a table by name
+ """
+ with sqlite3.connect("./data/main.db") as connection:
+ cursor = connection.cursor()
+
+ cursor.execute(f"SELECT * FROM {table};")
+
+ names = list(map(lambda x: x[0], cursor.description))
+ rows = [dict(zip(names, row)) for row in cursor.fetchall()]
+
+ return rows
+
+def read_views(table=None):
+ """
+ Reads views. If a table is specified, it reads all views related to that
+ table. If not it reads all views.
+ """
+ if table is None:
+ related = list_views()
+ else:
+ related = [view for view in list_views() if view.split("__view")[0] == table]
+
+ data = {}
+ for view in related:
+ for key, value in read(view)[0].items():
+ data.update({f"{view.replace('__view', '')}~~{key}": value})
+
+ return data
+
+def read_indexes():
+ with sqlite3.connect("./data/main.db") as connection:
+ cursor = connection.cursor()
+
+ cursor.execute("SELECT * FROM indexes;")
+
+ names = list(map(lambda x: x[0], cursor.description))
+ indexes = [dict(zip(names, row)) for row in cursor.fetchall()][0]
+
+ return indexes
--- /dev/null
+import sqlite3
+
+def integrity_check():
+ """
+ check the database is ok and return true if it is
+ """
+ with sqlite3.connect("./data/main.db") as connection:
+ cursor = connection.cursor()
+ integrity = cursor.execute('PRAGMA integrity_check')
+
+ return integrity.fetchone()[0] == "ok"
+
+def optimize():
+ """
+ check the database is ok and return true if it is
+ """
+ with sqlite3.connect("./data/main.db") as connection:
+ cursor = connection.cursor()
+ integrity = cursor.execute('PRAGMA optimize')
+
+def list_all():
+ """
+ get a list of all the tables and views in the database
+ """
+ with sqlite3.connect("./data/main.db") as connection:
+ cursor = connection.cursor()
+ cursor.execute("SELECT name FROM sqlite_master WHERE type IN ('table', 'view');")
+ return [table[0] for table in cursor.fetchall()]
+
+def list_views():
+ """
+ get a list of all the views in the database
+ """
+ with sqlite3.connect("./data/main.db") as connection:
+ cursor = connection.cursor()
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='view';")
+ return [table[0] for table in cursor.fetchall()]
--- /dev/null
+import sqlite3
+
+from .utils import list_all
+from .read import read
+
+def write(data):
+
+ with sqlite3.connect("./data/main.db") as connection:
+ cursor = connection.cursor()
+
+ for key in data:
+ table, index, feild = key.split("~~")
+
+ cursor.execute(f"""
+ UPDATE {table}
+ SET '{feild}' = '{data[key]}'
+ WHERE ROWID = {int(index) + 1};
+ """)
+
+ connection.commit()
--- /dev/null
+const outlineStyle: string = "4px solid green";
+const outlineTime: number = 500; // ms
+
+export function setupTrigger() {
+ // setup all trigger buttons to send post request on click
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("trigger");
+
+ for (let e of el) {
+ e.addEventListener("click", (event) => {
+ if (event.target instanceof HTMLElement) {
+ let time: number = Math.round(Date.now() / 1000);
+ let body: FormData = new FormData();
+ let delay = document.getElementById(`${event.target.getAttribute('name')}+delay`);
+
+ if (delay instanceof HTMLInputElement) {
+ body.set(`${event.target.getAttribute('name')}`, `${time}+${delay.value}`);
+
+ fetch("", {
+ method: "POST",
+ body: body
+ });
+
+ // trigger the button to turn green for approx same time (or 1s)
+ let timeout: number = delay.value as unknown as number * 1000;
+ if (timeout < outlineTime) { timeout = outlineTime; }
+
+ if ( event.target instanceof HTMLElement) {
+ event.target.style.outline = outlineStyle;
+ setTimeout(function () {
+ if ( event.target instanceof HTMLElement) {
+ event.target.style.outline = "none"
+ }
+ }, timeout);
+ }
+ }
+ }
+ })
+ }
+}
--- /dev/null
+export function renderDiscount(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("discount");
+
+ for (let e of el) {
+ if (e instanceof HTMLElement) {
+ if (typeof e.dataset.raw === "string") {
+ e.innerHTML = `${e.dataset.raw}%`;
+ }
+ }
+ }
+}
+
+export function renderReverseDiscount(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("reverseDiscount");
+
+ for (let e of el) {
+ if (e instanceof HTMLElement) {
+ if (typeof e.dataset.raw === "string") {
+ e.innerHTML = `${100 - Number(e.dataset.raw)}%`;
+ }
+ }
+ }
+}
--- /dev/null
+export default function renderPrice(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("price");
+
+ for (let e of el) {
+ if (e instanceof HTMLElement) {
+ if (typeof e.dataset.raw === "string") {
+ e.innerHTML = String(parseFloat(e.dataset.raw).toFixed(2));
+ }
+ }
+ }
+}
--- /dev/null
+export default function renderShowUntil(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("showUntil");
+
+ for (let e of el) {
+ if (e instanceof HTMLElement) {
+ if (typeof e.dataset.raw === "string") {
+ let duration: number = (eval(e.dataset.raw) * 1000) - Date.now();
+ if (duration > 0) {
+ e.classList.add("showUntil-showing")
+ setTimeout(function () {e.classList.remove("showUntil-showing")}, duration)
+ }
+ }
+ }
+ }
+}
--- /dev/null
+export function setupSounds() {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("sound");
+
+ const context = new AudioContext();
+ if (el.length > 0) {
+ for (let e of el) {
+ if (e instanceof HTMLMediaElement) {
+ const track = context.createMediaElementSource(e);
+ track.connect(context.destination);
+ }
+ }
+ }
+}
+
+export function playSounds() {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("sound");
+
+ for (let e of el) {
+ if (e instanceof HTMLMediaElement) {
+ if (typeof e.dataset.raw === "string") {
+ if (e.dataset.rawLast != e.dataset.raw) {
+ e.play();
+ }
+ e.dataset.rawLast = e.dataset.raw;
+ }
+ }
+ }
+}
--- /dev/null
+export default function renderState(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("state");
+
+ for (let e of el) {
+ if (e instanceof HTMLElement) {
+ if (typeof e.dataset.raw === "string") {
+ for (let className of e.classList) {
+ if (className.startsWith("state-")) {
+ e.classList.remove(className);
+ }
+ }
+
+ e.classList.add(`state-${e.dataset.raw}`);
+ }
+ }
+ }
+}
--- /dev/null
+export function setupStyle(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("style");
+
+ for (let i = 0; i < el.length; i++) {
+ const stylesheet = new CSSStyleSheet();
+ document.adoptedStyleSheets.push(stylesheet);
+ }
+}
+
+export function renderStyle(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("style");
+
+ for (let [i, e] of Array.from(el).entries()) {
+ if (e instanceof HTMLElement) {
+ let variable;
+ for (let className of e.classList) {
+ if (className.includes("--")) {
+ variable = className.split("--")[1];
+ }
+ }
+
+ if (typeof e.dataset.raw === "string") {
+ document.adoptedStyleSheets[i].replace(`
+ :root {--${variable}: ${e.dataset.raw};}
+ `);
+ }
+ }
+ }
+}
--- /dev/null
+export default function renderText(): void {
+ const el = document.querySelectorAll(".text,.title");
+ for (let e of el) {
+ if (e instanceof HTMLElement) {
+ if (typeof e.dataset.raw === "string") {
+ e.innerHTML = e.dataset.raw;
+ }
+ }
+ }
+}
--- /dev/null
+import { timeSince, timeUntil, timeFormatBroadcast } from "./utils.js";
+
+const timeUpdate: number = 1000; // ms
+
+export function setupTimeSince(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("since");
+
+ if (el.length > 0) {
+ setInterval(
+ function () {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("since");
+
+ for (let e of el) {
+ if (e instanceof HTMLElement) {
+ if (typeof e.dataset.raw === "string") {
+ let [hours, minutes, seconds] = timeSince(eval(e.dataset.raw))
+ e.innerHTML = timeFormatBroadcast(hours, minutes, seconds);
+ }
+ }
+ }
+ },
+ timeUpdate
+ );
+ }
+}
+
+export function setupTimeUntil(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("until");
+
+ if (el.length > 0) {
+ setInterval(
+ function () {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("until");
+
+ for (let e of el) {
+ if (e instanceof HTMLElement) {
+ if (typeof e.dataset.raw === "string") {
+ let [hours, minutes, seconds] = timeUntil(eval(e.dataset.raw))
+ e.innerHTML = timeFormatBroadcast(hours, minutes, seconds);
+ }
+ }
+ }
+ },
+ timeUpdate
+ );
+ }
+}
--- /dev/null
+import { io } from "socket.io-client";
+
+import { renderDiscount, renderReverseDiscount } from "./discount.js";
+import { setupTimeSince, setupTimeUntil } from "./time.js";
+import { setupSounds, playSounds } from "./sound.js";
+import { setupStyle, renderStyle } from "./style.js";
+import renderShowUntil from "./show_until.js";
+import renderState from "./state.js";
+import renderPrice from "./price.js"
+import renderText from "./text.js";
+
+export default function setupUpdate(): void {
+ setupTimeSince();
+ setupTimeUntil();
+ setupSounds();
+ setupStyle();
+
+ const socket = io();
+ socket.on("update", (data) => {
+ const start = performance.now()
+
+ for (const [key, value] of Object.entries(data)) {
+ const el = document.querySelectorAll(`.update[class~='${key}']`);
+
+ for (let e of el) {
+ if (e instanceof HTMLElement) {
+ e.dataset.raw = value as unknown as string;
+ }
+ }
+ }
+
+ renderReverseDiscount();
+ renderShowUntil();
+ renderDiscount();
+ renderState();
+ renderPrice();
+ renderStyle();
+ renderText();
+ playSounds();
+
+ console.log(`Updated in ${performance.now() - start}ms`);
+ });
+}
--- /dev/null
+export function timeUntil(end: number): number[] {
+ let current: number = Date.now() / 1000;
+ var time: number = end - current;
+
+ if (Math.sign(time) == -1) {time = 0;}
+
+ let hours: number = Math.floor(time / 3600);
+ time %= 3600;
+ let minutes: number = Math.floor(time / 60);
+ let seconds: number = Math.floor(time % 60);
+
+ return [hours, minutes, seconds]
+}
+
+export function timeSince(end: number): number[] {
+ let current: number = Date.now() / 1000;
+ var time: number = current - end;
+
+ if (Math.sign(time) == -1) {time = 0;}
+
+ let hours: number = Math.floor(time / 3600);
+ time %= 3600;
+ let minutes: number = Math.floor(time / 60);
+ let seconds: number = Math.floor(time % 60);
+
+ return [hours, minutes, seconds]
+}
+
+export function timeFormatBroadcast(hours: number, minutes: number, seconds: number): string {
+ var hoursString = hours.toString().padStart(2, "0");
+ var minutesString = minutes.toString().padStart(2, "0");
+ var secondsString = seconds.toString().padStart(2, "0");
+
+ return `${hoursString}h ${minutesString}' ${secondsString}"`
+}
--- /dev/null
+/* PAGE POSITIONING */
+
+:root {
+ --nav-background: lightgray;
+}
+
+body > * {
+ padding: 15px;
+ overflow: scroll
+}
+nav {
+ position: absolute;
+ top: 0; bottom: 0; left: 0;
+ right: max(80%, calc(100vw - 350px));
+ background-color: var(--nav-background);
+}
+main {
+ position: absolute;
+ top: 0; bottom: 0; right: 0;
+ left: min(20%, calc(350px));
+}
+
+/* THE OPTIONS GRID */
+
+.optionsGrid {
+ display: grid;
+ grid-template-columns: 50px auto auto auto;
+}
+.optionsGrid > * {
+ padding: 10px;
+ border: 1px solid gray;
+ margin-top: -1px;
+ margin-right: -1px;
+}
+.optionsGridSpacer { border: none; }
+
+
+:target {
+ outline: 4px solid blue;
+ z-index: 1;
+}
+.currentValue { color: red; }
+.currentValue[type="radio"] { background-color: red; }
+
+i {
+ font-style: normal;
+ color: white;
+ text-shadow:
+ 1px 1px 0 black,
+ -1px 1px 0 black,
+ 1px -1px 0 black,
+ -1px -1px 0 black,
+ 0px 1px 0 black,
+ 0px -1px 0 black,
+ 1px 0px 0 black,
+ -1px 0px 0 black;
+}
+
+/* PRINT MODE
+ need to work on this a little bit, currently just hides things that
+ shouldnt be printed and thats all. Should be able to potentially display
+ extra elements or manipulate the page in a way that make it easy to read
+ or annotate on the page.
+ */
+
+@media ( max-width: 1000px ) {
+ nav { position: static; }
+ main { position: static; }
+}
+
+@media print {
+ nav, [type="radio"] { display: none }
+ [type="submit"] { opacity: 0; }
+ main { position: static; }
+ .currentValue { color: black; }
+ .optionsGrid { grid-template-columns: 0 auto 0 auto; }
+ .optionsGrid > * { padding: 0; }
+ .optionsGridSpacer { padding: 20px; }
+}
--- /dev/null
+/*\r
+This file is from the Andika project (https://software.sil.org/andika/).\r
+Copyright (c) 2004-2025 SIL Global (https://www.sil.org/) with Reserved\r
+Font Names "Andika" and "SIL". This Font Software is licensed under the SIL\r
+Open Font License, Version 1.1 (https://openfontlicense.org).\r
+*/\r
+\r
+@font-face {\r
+ font-family: "Andika";\r
+ font-weight: 400;\r
+ src: url(/static/fonts/Andika-Regular.woff2);\r
+}\r
+@font-face {\r
+ font-family: "Andika";\r
+ font-weight: 500;\r
+ src: url(/static/fonts/Andika-Medium.woff2);\r
+}\r
+@font-face {\r
+ font-family: "Andika";\r
+ font-weight: 600;\r
+ src: url(/static/fonts/Andika-SemiBold.woff2);\r
+}\r
+@font-face {\r
+ font-family: "Andika";\r
+ font-weight: 700;\r
+ src: url(/static/fonts/Andika-Bold.woff2);\r
+}\r
+@font-face {\r
+ font-family: "Andika";\r
+ font-weight: 400;\r
+ font-style: italic;\r
+ src: url(/static/fonts/Andika-Italic.woff2);\r
+}\r
+@font-face {\r
+ font-family: "Andika";\r
+ font-weight: 500;\r
+ font-style: italic;\r
+ src: url(/static/fonts/Andika-MediumItalic.woff2);\r
+}\r
+@font-face {\r
+ font-family: "Andika";\r
+ font-weight: 600;\r
+ font-style: italic;\r
+ src: url(/static/fonts/Andika-SemiBoldItalic.woff2);\r
+}\r
+@font-face {\r
+ font-family: "Andika";\r
+ font-weight: 700;\r
+ font-style: italic;\r
+ src: url(/static/fonts/Andika-BoldItalic.woff2);\r
+}\r
+\r
+:root {\r
+ font-family: "Andika", sans-serif;\r
+}\r
--- /dev/null
+:root {
+ font-family: sans-serif;
+ color: white;
+ background-color: black;
+}
+
+.infoA { font-size: var(--aFontSize); }
+.infoB { font-size: var(--bFontSize); }
+.infoC { font-size: var(--cFontSize); }
+
+.title { text-transform: capitalize; }
+
+/* INFO A */
+.infoA div > * { padding: 20px; }
+.infoA div {
+ position: absolute;
+ top: 0; left: 0; bottom: 0; right: 0;
+ display: grid;
+ grid: none / 1fr;
+}
+
+/* INFO B */
+.infoB div > * { padding: 20px; }
+.infoB div {
+ position: absolute;
+ top: 0; left: 0; bottom: 0; right: 0;
+ display: grid;
+ grid: none / 1fr 1fr;
+}
+
+/* INFO C */
+.infoC div > * { padding: 20px; text-align: center; }
+.infoC div {
+ position: absolute;
+ top: 0; left: 0; bottom: 0; right: 0;
+ display: grid;
+ grid: none / 6fr 1fr 6fr;
+}
+
--- /dev/null
+:root {
+ font-size: var(--baseFontSize);
+ font-weight: 600;
+}
+.title { text-transform: capitalize; }
--- /dev/null
+body:not(.state-sold, .state-move) .styleBannerBox {
+ padding: 0; margin: 0;
+}
+.styleBannerBox {
+ opacity: 0; max-height: 0;
+ transform: translateX(-20px);
+ /* should do some tweeking for the below */
+ transition:
+ opacity var(--baseAnimationLength),
+ max-height var(--baseAnimationLength) cubic-bezier(1,0,0,1),
+ padding var(--baseAnimationLength),
+ margin var(--baseAnimationLength),
+ transform var(--baseAnimationLength);
+ overflow: hidden;
+}
+body.state-sold .styleBannerBox,
+body.state-move .styleBannerBox {
+ opacity: 1; max-height: 50vh;
+ transform: translateX(0px);
+}
--- /dev/null
+:root {
+ --boxHPad: 6px;
+ --boxVPad: 2px;
+ --boxHMar: 0px;
+ --boxVMar: 5px;
+}
+.boxA {
+ width: calc(100% - 2 * (var(--boxHMar) + var(--boxHPad) + var(--borderASize)));
+
+ color: var(--textAColor);
+ text-shadow: var(--textAShadow);
+
+ background: var(--backgroundA);
+ box-shadow: inset 0 0 var(--innerGlowSize) var(--innerGlowColor);
+ border: var(--borderASize) solid var(--borderAColor);
+
+ border-radius: var(--borderARadius);
+ padding: var(--boxVPad) var(--boxHPad) var(--boxVPad) var(--boxHPad);
+ margin: var(--boxVMar) var(--boxHMar) var(--boxVMar) var(--boxHMar);
+}
+.boxA.styleExpand.showUntil:not(.showUntil-showing) {
+ margin-bottom: calc(-2 * var(--borderASize));
+}
+
+.boxB {
+ width: calc(100% - 2 * (var(--boxHMar) + var(--boxHPad) + var(--borderBSize)));
+
+ color: var(--textBColor);
+ text-shadow: var(--textBShadow);
+
+ background: var(--backgroundB);
+ box-shadow: inset 0 0 var(--innerGlowSize) var(--innerGlowColor);
+ border: var(--borderBSize) solid var(--borderBColor);
+
+ border-radius: var(--borderBRadius);
+ padding: var(--boxVPad) var(--boxHPad) var(--boxVPad) var(--boxHPad);
+ margin: var(--boxVMar) var(--boxHMar) var(--boxVMar) var(--boxHMar);
+}
+.boxB.styleExpand.showUntil:not(.showUntil-showing) {
+ margin-bottom: calc(-2 * var(--borderBSize));
+}
+
+.boxC {
+ width: calc(100% - 2 * (var(--boxHMar) + var(--boxHPad) + var(--borderCSize)));
+
+ color: var(--textCColor);
+ text-shadow: var(--textCShadow);
+
+ background: var(--backgroundC);
+ box-shadow: inset 0 0 var(--innerGlowSize) var(--innerGlowColor);
+ border: var(--borderCSize) solid var(--borderCColor);
+
+ border-radius: var(--borderCRadius);
+ padding: var(--boxVPad) var(--boxHPad) var(--boxVPad) var(--boxHPad);
+ margin: var(--boxVMar) var(--boxHMar) var(--boxVMar) var(--boxHMar);
+}
+.boxC.styleExpand.showUntil:not(.showUntil-showing) {
+ margin: 0 0 calc(-2 * var(--borderCSize)) 0;
+}
--- /dev/null
+.styleCenterShout {
+ position: absolute;
+ top: 0; left: 0; bottom: 0; right: 0;
+
+ display: flex;
+ align-items: center; justify-content: center;
+
+ text-transform: uppercase;
+}
+.styleCenterShout > div { background-color: red; }
+
+.styleCenterShout.showUntil > div {
+ transform: translateX(-100px);
+ opacity: 0;
+ transition:
+ transform var(--baseAnimationLength),
+ opacity var(--baseAnimationLength);
+}
+.styleCenterShout.showUntil > div > span {
+ display: block;
+ transform: translateX(200px);
+ transition: transform var(--baseAnimationLength);
+
+ text-transform: uppercase;
+}
+.styleCenterShout.showUntil-showing * {
+ opacity: 1 !important;
+ transform: translateX(0px) !important;
+}
--- /dev/null
+.styleDiscountBox > em {
+ display: block;
+ font-size: 0.7em;
+ line-height: 1.4em;
+}
+.styleDiscountBox > div {
+ font-size: 1.2em;
+}
+
+body:not(.state-discount, .state-move) .styleDiscountBox {
+ padding: 0; margin: 0;
+}
+.styleDiscountBox {
+ opacity: 0; max-height: 0;
+ transform: translateX(-20px);
+ /* should do some tweeking for the below */
+ transition:
+ opacity var(--baseAnimationLength),
+ max-height var(--baseAnimationLength) cubic-bezier(1,0,0,1),
+ padding var(--baseAnimationLength),
+ margin var(--baseAnimationLength),
+ transform var(--baseAnimationLength);
+ overflow: hidden;
+}
+body.state-discount .styleDiscountBox,
+body.state-move .styleDiscountBox {
+ opacity: 1; max-height: 50vh;
+ transform: translateX(0px);
+}
+
--- /dev/null
+body:not(
+ .state-product,
+ .state-price,
+ .state-discount,
+ .state-move,
+ .state-sold
+) .styleItemBox {
+ padding: 0; margin: 0;
+}
+.styleItemBox {
+ opacity: 0; max-height: 0;
+ transform: translateX(-20px);
+ /* should do some tweeking for the below */
+ transition:
+ opacity var(--baseAnimationLength),
+ max-height var(--baseAnimationLength) cubic-bezier(1,0,0,1),
+ padding var(--baseAnimationLength),
+ margin var(--baseAnimationLength),
+ transform var(--baseAnimationLength);
+ overflow: hidden;
+}
+body.state-product .styleItemBox,
+body.state-price .styleItemBox,
+body.state-discount .styleItemBox,
+body.state-move .styleItemBox,
+body.state-sold .styleItemBox {
+ opacity: 1; max-height: 50vh;
+ transform: translateX(0px);
+}
--- /dev/null
+body:not(
+ .state-crawler,
+ .state-product
+) .styleLogoBox {
+ padding: 0; margin: 0;
+}
+.styleLogoBox {
+ opacity: 0; max-height: 0;
+ transform: translateX(-20px);
+ /* should do some tweeking for the below */
+ transition:
+ opacity var(--baseAnimationLength),
+ max-height var(--baseAnimationLength) cubic-bezier(1,0,0,1),
+ padding var(--baseAnimationLength),
+ margin var(--baseAnimationLength),
+ transform var(--baseAnimationLength);
+ overflow: hidden;
+}
+body.state-product .styleLogoBox,
+body.state-crawler .styleLogoBox {
+ opacity: 1; max-height: 50vh;
+ transform: translateX(0px);
+}
--- /dev/null
+body:not(.state-price, .state-discount, .state-move) .stylePricingBox {
+ padding: 0; margin: 0;
+}
+.stylePricingBox {
+ opacity: 0; max-height: 0;
+ transform: translateX(-20px);
+ /* should do some tweeking for the below */
+ transition:
+ opacity var(--baseAnimationLength),
+ max-height var(--baseAnimationLength) cubic-bezier(1,0,0,1),
+ padding var(--baseAnimationLength),
+ margin var(--baseAnimationLength),
+ transform var(--baseAnimationLength);
+ overflow: hidden;
+}
+body.state-price .stylePricingBox,
+body.state-discount .stylePricingBox,
+body.state-move .stylePricingBox {
+ opacity: 1; max-height: 50vh;
+ transform: translateX(0px);
+}
--- /dev/null
+.styleQtyBox {
+ font-size: 0.7em;
+}
+
+body:not(.state-sold, .state-move) .styleQtyBox {
+ padding: 0; margin: 0;
+}
+.styleQtyBox {
+ opacity: 0; max-height: 0;
+ transform: translateX(-20px);
+ /* should do some tweeking for the below */
+ transition:
+ opacity var(--baseAnimationLength),
+ max-height var(--baseAnimationLength) cubic-bezier(1,0,0,1),
+ padding var(--baseAnimationLength),
+ margin var(--baseAnimationLength),
+ transform var(--baseAnimationLength);
+ overflow: hidden;
+}
+body.state-sold .styleQtyBox,
+body.state-move .styleQtyBox {
+ opacity: 1; max-height: 50vh;
+ transform: translateX(0px);
+}
--- /dev/null
+.styleExpand.showUntil:not(.showUntil-showing) {
+ padding: 0; margin: 0;
+}
+.styleExpand.showUntil {
+ opacity: 0; max-height: 0;
+ transform: translateX(-20px);
+ /* should do some tweeking for the below */
+ transition:
+ opacity var(--baseAnimationLength),
+ max-height var(--baseAnimationLength) cubic-bezier(1,0,0,1),
+ padding var(--baseAnimationLength),
+ margin var(--baseAnimationLength),
+ transform var(--baseAnimationLength);
+ overflow: hidden;
+}
+.styleExpand.showUntil.showUntil-showing {
+ opacity: 1; max-height: 50vh;
+ transform: translateX(0px);
+}
--- /dev/null
+.styleSideBox {
+ width: var(--sideBoxWidth);
+
+ display: flex;
+ flex-direction: column;
+
+ margin: 50px;
+}
+.styleSideBox > * {
+ margin-bottom: 5px;
+ width: 100%;
+}
--- /dev/null
+.styleSideTimerBox {
+ display: inline;
+ width: fit-content !important;
+}
--- /dev/null
+{#
+
+ builds the admin page
+
+#}
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>δ Velorum</title>
+ <link rel="stylesheet" href="/static/admin.css">
+
+ <script type="module" defer>
+
+import { setupTrigger } from "/script/admin.js";
+setupTrigger();
+
+ </script>
+ </head>
+ <body>
+ <nav>
+ <p><b>Tables</b></p>
+ <ul style="border: 1px solid gray">
+ {% for table in indexes %}
+ {% set table_name = table.split("--")[1] %}
+ <li><a href="/admin/{{ table_name }}">{{ table_name }}</a></li>
+ {% endfor %}
+ </ul>
+ </nav>
+ <main>
+ <h1><i>δ Velorum {{ star }}</i> “Alsephina” <em>
+ {{ {"Aa": "The Ship", "Ab": "The Liferaft", "B": "Unknown"}[star] }}
+ </em></h1>
+
+ <form method="GET" action="/db">
+ <button type="submit">Donwload database</button>
+ </form>
+ <form method="POST" action="/db">
+ <input type="file"/>
+ <button type="submit">Upload database</button>
+ </form>
+
+ <ul>
+ <li><a href="/">Main GFX overlay</a></li>
+ <li><a href="/info/a">Info page A</a> <em>(Item name, description, Anchor notes)</em></li>
+ <li><a href="/info/b">Info page B</a> <em>(Price, discount and stock)</em></li>
+ <li><a href="/info/c">Info page C</a> <em>(Gallery screen, timing and state)</em></li>
+ </ul>
+
+ <img
+ src="/static/media/velo.jpg"
+ attributionsrc="https://commons.wikimedia.org/w/index.php?curid=4075468"
+ style="width: min(100%, 700px);" />
+ </main>
+ </body>
+</html>
--- /dev/null
+<label>{{ label }}</label>
+<span>
+ <input id="{{ name }}" type="color" name="{{ name }}" value="{{ value }}" />
+ {{ value }}
+</span>
+<span class="currentValue" style="background-color: {{ value }};"></span>
--- /dev/null
+<label>{{ label }}</label>
+<input id="{{ name }}" type="number" name="{{ name }}" value="{{ value }}" step="0.01" />
+<span class="currentValue">{{ value }}</span>
--- /dev/null
+<label>{{ label }}</label>
+<input id="{{ name }}" type="number" name="{{ name }}" value="{{ value }}" />
+<span class="currentValue">{{ value }}</span>
--- /dev/null
+<label>{{ label }}</label>
+<input id="{{ name }}" type="text" name="{{ name }}" value="{{ value }}" />
+<span class="currentValue">{{ value }}</span>
+
--- /dev/null
+<label>{{ label }}</label>
+<textarea id="{{ name }}" style="grid-column: 3 / span 2;" name="{{ name }}">
+ {{- value -}}
+</textarea>
--- /dev/null
+<span id="{{ name }}" style="grid-column: 2 / span 3;">
+ <noscript style="color: red;">Trigger will not function without JS<br></noscript>
+ <button type="button" name="{{ name }}" value="{{ label }}" class="trigger">
+ {{ label }}
+ </button>
+ <input type="number" id="{{ name }}+delay" value="{{ value.split('+')[1] }}"/><span> seconds delay <em>(This does not update the triggered data.)</em></span>
+</span>
--- /dev/null
+<!doctype html>
+<html>
+ <head>
+ <link rel="stylesheet" href="/static/style.css">
+ <link rel="stylesheet" href="/static/andika.css">
+
+ {# ELEMENT STYLING IMPORTS #}
+ <link rel="stylesheet" href="/static/style/side_box.css">
+ <link rel="stylesheet" href="/static/style/qty_box.css">
+ <link rel="stylesheet" href="/static/style/side_timer_box.css">
+ <link rel="stylesheet" href="/static/style/center_shout.css">
+ <link rel="stylesheet" href="/static/style/discount_box.css">
+ <link rel="stylesheet" href="/static/style/banner_box.css">
+ <link rel="stylesheet" href="/static/style/pricing_box.css">
+ <link rel="stylesheet" href="/static/style/item_box.css">
+ <link rel="stylesheet" href="/static/style/logo_box.css">
+
+ {# HELPER IMPORTS #}
+ <link rel="stylesheet" href="/static/style/box.css">
+ <link rel="stylesheet" href="/static/style/show_until.css">
+ <link rel="stylesheet" href="/static/style/background.css">
+
+ <script type="importmap">
+ {
+ "imports": {
+ "socket.io-client": "https://cdn.socket.io/4.8.3/socket.io.esm.min.js"
+ }
+ }
+ </script>
+ <script type="module" defer>
+
+import setupUpdate from "/script/update.js";
+setupUpdate();
+
+ </script>
+ </head>
+ <body class="update data__state~~text--state state">
+
+ {# GLOBAL STYLING ELEMENTS #}
+ <span class="update style~~text--baseAnimationLength style"></span>
+ <span class="update style~~text--sideBoxWidth style"></span>
+ <span class="update style~~text--baseFontSize style"></span>
+
+ {# DYNAMIC STYLING ELEMENTS #}
+
+ <span class="update style__colorscheme~~text--borderASize style"></span>
+ <span class="update style__colorscheme~~color--borderAColor style"></span>
+ <span class="update style__colorscheme~~text--borderBSize style"></span>
+ <span class="update style__colorscheme~~color--borderBColor style"></span>
+ <span class="update style__colorscheme~~text--borderCSize style"></span>
+ <span class="update style__colorscheme~~color--borderCColor style"></span>
+
+ <span class="update style__colorscheme~~color--textAColor style"></span>
+ <span class="update style__colorscheme~~color--textBColor style"></span>
+ <span class="update style__colorscheme~~color--textCColor style"></span>
+ <span class="update style__colorscheme~~text--textAShadow style"></span>
+ <span class="update style__colorscheme~~text--textBShadow style"></span>
+ <span class="update style__colorscheme~~text--textCShadow style"></span>
+
+ <span class="update style__colorscheme~~text--borderARadius style"></span>
+ <span class="update style__colorscheme~~text--borderBRadius style"></span>
+ <span class="update style__colorscheme~~text--borderCRadius style"></span>
+
+ <span class="update style__colorscheme__backgroundA~~text--backgroundA style"></span>
+ <span class="update style__colorscheme__backgroundB~~text--backgroundB style"></span>
+ <span class="update style__colorscheme__backgroundC~~text--backgroundC style"></span>
+
+ <span class="update style__colorscheme~~color--innerGlowColor style"></span>
+ <span class="update style__colorscheme~~text--innerGlowSize style"></span>
+
+ {# SFX ELEMENTS #}
+ <audio src="/static/media/sfx/banner_shout.wav" class="update data~~trigger--bannerShoutTrigger sound"></audio>
+ <audio src="/static/media/sfx/clock.wav" class="update data__clock~~number--positionDegrees sound"></audio>
+ <audio src="/static/media/sfx/state_change.wav" class="update data__state~~text--state sound"></audio>
+ <audio src="/static/media/sfx/shout.wav" class="update data~~trigger--centerShoutTrigger sound"></audio>
+
+ {# CENTER SHOUT #}
+ <div class="styleCenterShout update data~~trigger--centerShoutTrigger showUntil">
+ <div>
+ <span class="update data~~text--centerShoutText text"></span>
+ </div>
+ </div>
+
+ {# SIDE BOX
+ includes (top to bottom):
+ - LOGO BOX (show during: crawler, product)
+ - BANNER BOX (show during: move, sold)
+ - ITEM BOX (show during: product, price, discount, move, sold)
+ - PRICING BOX (show during: price, discount, move)
+ - DISCOUNT BOX (show during: discount, move)
+ - BANNER SHOUT BOX (show AD)
+ - QTY BOX (show during: move, sold)
+ - TIMER 1 BOX (show AD)
+ - TIMER 2 BOX (show AD)
+
+ these are switched on and off using the state class from the
+ body tag.
+ #}
+ <div class="styleSideBox">
+
+ {# LOGO BOX #}
+ <div class="styleLogoBox">
+ LOGO BOX
+ </div>
+
+ {# BANNER BOX #}
+ <div class="styleBannerBox boxC">
+ BANNER BOX
+ </div>
+
+ {# ITEM BOX #}
+ <div class="styleItemBox boxB">
+ <div class="boxA">
+ ITEM BOX
+ </div>
+ </div>
+
+ {# PRICING BOX #}
+ <div class="stylePricingBox boxA">
+ PRICING BOX
+ </div>
+
+ {# DISOUNT BOX #}
+ <div class="styleDiscountBox boxB">
+ <em>Now only...</em>
+ <div class="boxA">
+ <b><span class="update data__product~~number--discount discount"></span></b>
+ OFF!
+ </div>
+ </div>
+
+ {# BANNER SHOUT BOX #}
+ <div class="styleExpand boxC update data~~trigger--bannerShoutTrigger showUntil">
+ <span class="update data~~text--bannerShoutText title"></span>
+ </div>
+
+ {# QTY BOX #}
+ <div class="styleQtyBox boxB">
+ <em>Sold:
+ <span class="update data__product~~number--quantityTotal text"></span>...</em>
+ only
+ <span class="update data__product~~number--quantityCurrent text"></span>
+ LEFT
+ </div>
+
+ {# TIMER BOX #}
+ <div class="styleSideTimerBox boxA">
+ TIMER 1!
+ </div>
+ <div class="styleSideTimerBox boxA">
+ TIMER 2!
+ </div>
+ </div>
+ </body>
+</html>
--- /dev/null
+<!doctype html>
+<html>
+ <head>
+ <link rel="stylesheet" href="/static/info.css">
+ <script type="importmap">
+ {
+ "imports": {
+ "socket.io-client": "https://cdn.socket.io/4.8.3/socket.io.esm.min.js"
+ }
+ }
+ </script>
+ <script type="module" defer>
+
+import setupUpdate from "/script/update.js";
+setupUpdate();
+
+ </script>
+ </head>
+ <body class="infoA">
+ <span class="update style~~text--aFontSize style"></span>
+
+ <div>
+ <span>
+ <u style="color: pink;"><span class="update data__product~~text--name text"></span></u><br>
+ <span style="color: pink" class="update data__product~~text--description text"></span>
+ </span>
+
+ <span class="styleProductNotes update data__product~~textarea--notes text"></span>
+
+ <span style="color: yellow" class="update data~~textarea--anchorNotes text"></span>
+ </div>
+ </body>
+</html>
--- /dev/null
+<!doctype html>
+<html>
+ <head>
+ <link rel="stylesheet" href="/static/info.css">
+ <script type="importmap">
+ {
+ "imports": {
+ "socket.io-client": "https://cdn.socket.io/4.8.3/socket.io.esm.min.js"
+ }
+ }
+ </script>
+ <script type="module" defer>
+
+import setupUpdate from "/script/update.js";
+setupUpdate();
+
+ </script>
+ </head>
+ <body class="infoB">
+ <span class="update style~~text--bFontSize style"></span>
+
+ <div>
+ <span>
+ <span style="background-color: green" class="update data__product__currentPriceString~~text--currentPriceString text"></span> <em>(was <span class="update data__product__originalPriceString~~text--originalPriceString text"></span>)</em>
+ </span>
+
+ <span>
+ <span class="update data__product~~number--quantityCurrent text"></span>
+ left of
+ <span class="update data__product~~number--quantityTotal text"></span>
+ total
+ </span>
+
+ <span>
+ <span style="background-color: purple;" class="update data__product~~number--discount discount"></span> off
+ </span>
+
+ <span>
+ <span style="background-color: purple;" class="update data__product~~number--discount reverseDiscount"></span> full price
+ </span>
+
+ <span style="grid-column: 1 / span 2;">
+ Target: <span style="background-color: orange;" class="update data__product~~number--targetDiscount discount"></span> (<span class="update data__product__remainingDiscount~~number--remainingDiscount discount"></span> left to drop)
+ </span>
+
+ <span style="grid-column: 1 / span 2;">
+ Max: <span style="background-color: red;" class="update data__product~~number--maximumDiscount discount"></span> (<span class="update data__product__remainingMaximumDiscount~~number--remainingMaximumDiscount discount"></span> left to drop)
+ </span>
+ </div>
+ </body>
+</html>
--- /dev/null
+<!doctype html>
+<html>
+ <head>
+ <link rel="stylesheet" href="/static/info.css">
+ <script type="importmap">
+ {
+ "imports": {
+ "socket.io-client": "https://cdn.socket.io/4.8.3/socket.io.esm.min.js"
+ }
+ }
+ </script>
+ <script type="module" defer>
+
+import setupUpdate from "/script/update.js";
+setupUpdate();
+
+ </script>
+ </head>
+ <body class="infoC">
+ <span class="update style~~text--cFontSize style"></span>
+
+ <div>
+ <span>Current product:<br><span style="background-color: blue;" class="update data__product~~text--name text"></span></span>
+ <span>→</span>
+ <span>Next product:<br><span style="background-color: blue;" class="update data__product__next~~text--name text"></span></span>
+
+ <span>Current state: <span class="update data__state~~text--state title"></span></span>
+ <span>→</span>
+ <span>Next state: <span class="update data__state~~text--nextState title"></span></span>
+
+ <span style="grid-column: 1 / span 3; font-size: 2em; font-size: 2em;">
+ <span class="update data~~trigger--startTrigger since"></span> <span style="background-color: red;">T-<span class="update data__endTime~~text--endTime until"></span></span>
+ </span>
+ </div>
+ </body>
+</html>
--- /dev/null
+{#
+
+ builds the admin page
+
+#}
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>δ Velorum</title>
+ <link rel="stylesheet" href="/static/admin.css">
+
+ <script type="module" defer>
+
+import { setupTrigger } from "/script/admin.js";
+setupTrigger();
+
+ </script>
+ </head>
+ <body>
+ <nav>
+ <p><a href="/admin">.. back</a></p>
+ <p><b>{{ table_name }}</b></p>
+ <ol>
+ {% for row in table %}
+ <li style="border: 1px solid gray; margin-top: -1px;"><ul>
+ {% set group = loop.index0 %}
+ {% for key in row %}
+ <li><a href="#{{ table_name ~ "~~" ~ group ~ "~~" ~ key }}">{{ key }}</a></li>
+ {% endfor %}
+ </ul></li>
+ {% endfor %}
+ </ol>
+ </nav>
+ <main>
+ <h1><i>δ Velorum {{ star }}</i> Table: {{ table_name }}</h1>
+ <form class="optionsGrid" method="post">
+ <input type="submit" value="Submit table" style="grid-column: 1 / span 4;">
+ <span class="optionsGridSpacer" style="grid-column: 1 / span 4;"></span>
+ {% for row in table %}
+
+ {# CREATE RADIO BUTTON FOR ROW #}
+
+ <span style="
+ grid-row: {{ ((row|length + 1) * loop.index0) + 3 }} / span {{ row|length }};
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ ">
+ {% if loop.index0 == indexes["int--" ~ table_name] %}
+ <input type="radio" name="{{ "indexes~~0~~int--" ~ table_name }}" value="{{ loop.index0 }}" checked="checked" class="currentValue">
+ {% else %}
+ <input type="radio" name="{{ "indexes~~0~~int--" ~ table_name }}" value="{{ loop.index0 }}" >
+ {% endif %}
+ </span>
+
+ {# CREATE FEILDS #}
+
+ {% set group = loop.index0 %}
+ {% for feild in row %}
+
+ {# SELECT ELEMENT FROM TYPE #}
+
+ {% set type, label = feild.split('--') %}
+ {% set value = row[feild] %}
+ {% set name = table_name ~ "~~" ~ group ~ "~~" ~ feild %}
+
+ {% if type == "textarea" %}
+ {% include "admin/textarea.html" %}
+ {% elif type == "text" %}
+ {% include "admin/text.html" %}
+ {% elif type == "number" %}
+ {% include "admin/number.html" %}
+ {% elif type == "float" %}
+ {% include "admin/float.html" %}
+ {% elif type == "trigger" %}
+ {% include "admin/trigger.html" %}
+ {% elif type == "color" %}
+ {% include "admin/color.html" %}
+ {% endif %}
+ {% endfor %}
+ <span class="optionsGridSpacer" style="grid-column: 1 / span 4;"></span>
+
+ {% endfor %}
+ <input type="submit" value="Submit table" style="grid-column: 1 / span 4;">
+ </form>
+
+ <code>
+ Note: Tables are rendered statically, it is not safe to opperate the same interface in two places at once.
+ <br>
+ Processed {{ table|length * table[0]|length }} feilds in
+ {% if (time_taken * 1000)|round(2) > 20 %}
+ <span style="color: red;">{{ (time_taken * 1000)|round(2) }}ms</span>
+ {% else %}
+ {{ (time_taken * 1000)|round(2) }}ms
+ {% endif %}
+ </code>
+ </main>
+ </body>
+</html>
--- /dev/null
+def deindex(data, indexes):
+ deindexed = {}
+ for key in data:
+ table, index, feild = key.split("~~")
+ if table != "indexes" and int(index) == indexes[f"int--{table}"]:
+ deindexed.update({
+ f"{table}~~{feild}": data[key]
+ })
+
+ return deindexed
+++ /dev/null
-from werkzeug.security import generate_password_hash, check_password_hash
-from flask import Flask, render_template, send_from_directory, request
-from flask_socketio import SocketIO, emit
-from flask_httpauth import HTTPBasicAuth
-from flask_cors import CORS
-import json
-import os
-
-from .utils import deindex
-
-import director.admin as admin
-
-from director.database.read import read_views as database_read_views
-
-app = Flask(__name__, instance_relative_config=False)
-socketio = SocketIO(app, logger=False, engineio_logger=False)
-auth = HTTPBasicAuth()
-CORS(app)
-
-# import authentication data
-
-with open(f"{app.root_path}/../../secrets.json", "r", encoding="utf-8") as f:
- users = json.loads(f.read())
-users = {k: generate_password_hash(v) for (k, v) in users.items()}
-
-# password verifier
-
-def verify_password(username, password):
- if username in users and check_password_hash(users.get(username), password):
- return username
-
-auth.verify_password(verify_password)
-
-# root movements
-
-@app.route("/", methods=['get'])
-def index():
- return render_template("index.html")
-
-@socketio.on('connect')
-def connecting():
- emit("update", database_read_views(), json=True, namespace="/", broadcast=True)
-
-# info screen pages
-
-@app.route("/info/<string:screen>", methods=["get"])
-def info(screen):
- return render_template(f"info/{screen}.html")
-
-# admin screen pages
-
-app.route("/admin", methods=["get"])(auth.login_required(admin.admin_main))
-app.route("/admin/<string:table_name>", methods=["get", "post"])(auth.login_required(admin.admin_table))
-
-# script pages
-
-@app.route("/script/<path:filename>", methods=["get"])
-def script(filename):
- return send_from_directory(os.path.join(app.root_path, "build"), filename)
-
-@app.route("/db", methods=["get"])
-@auth.login_required
-def download_db():
- return send_from_directory(f"{app.root_path}/../data", "main.db")
+++ /dev/null
-from flask import render_template, request
-from flask_socketio import emit
-import time
-
-from director.database.read import read_indexes as database_read_indexes
-from director.database.read import read_views as database_read_views
-from director.database.write import write as database_write
-from director.database.read import read as database_read
-
-def admin_main():
- return render_template("admin.html", indexes=database_read_indexes())
-
-def admin_table(table_name):
- start = time.time()
-
- if request.method == "POST":
- database_write(request.form)
-
- emit(
- "update",
- database_read_views(table_name),
- json=True, namespace="/", broadcast=True
- )
-
- end = time.time()
-
- return render_template(
- "table.html",
- indexes=database_read_indexes(),
- table=database_read(table_name),
- table_name=table_name,
- time_taken = end - start
- )
+++ /dev/null
-"""
- package to read and write to the database
-
- check if file exists and pragam check the database on init
-"""
-
-from .utils import integrity_check, optimize
-import sqlite3
-import os
-
-assert os.path.exists("./data/main.db"), "Database missing"
-assert integrity_check(), "Database integrity error"
-
-optimize()
+++ /dev/null
-import sqlite3
-
-from .utils import list_all, list_views
-
-def read_all_current():
- tables = list_all()
- indexes = read_indexes()
-
- with sqlite3.connect("./data/main.db") as connection:
- cursor = connection.cursor()
-
- data = {}
- for table in tables:
- if table == "indexes": continue
-
- if "__view" in table:
- index_table = table.split("__view")[0]
- else: index_table = table
-
- cursor.execute(f"SELECT * FROM {table};")
-
- names = list(map(lambda x: f"{table}~~{x[0]}", cursor.description))
- row = cursor.fetchall()[indexes[f"int--{index_table}"]]
-
- data.update(dict(zip(names, row)))
-
- return data
-
-def read(table):
- """
- reads a table by name
- """
- with sqlite3.connect("./data/main.db") as connection:
- cursor = connection.cursor()
-
- cursor.execute(f"SELECT * FROM {table};")
-
- names = list(map(lambda x: x[0], cursor.description))
- rows = [dict(zip(names, row)) for row in cursor.fetchall()]
-
- return rows
-
-def read_views(table=None):
- """
- Reads views. If a table is specified, it reads all views related to that
- table. If not it reads all views.
- """
- if table is None:
- related = list_views()
- else:
- related = [view for view in list_views() if view.split("__view")[0] == table]
-
- data = {}
- for view in related:
- for key, value in read(view)[0].items():
- data.update({f"{view.replace('__view', '')}~~{key}": value})
-
- return data
-
-def read_indexes():
- with sqlite3.connect("./data/main.db") as connection:
- cursor = connection.cursor()
-
- cursor.execute("SELECT * FROM indexes;")
-
- names = list(map(lambda x: x[0], cursor.description))
- indexes = [dict(zip(names, row)) for row in cursor.fetchall()][0]
-
- return indexes
+++ /dev/null
-import sqlite3
-
-def integrity_check():
- """
- check the database is ok and return true if it is
- """
- with sqlite3.connect("./data/main.db") as connection:
- cursor = connection.cursor()
- integrity = cursor.execute('PRAGMA integrity_check')
-
- return integrity.fetchone()[0] == "ok"
-
-def optimize():
- """
- check the database is ok and return true if it is
- """
- with sqlite3.connect("./data/main.db") as connection:
- cursor = connection.cursor()
- integrity = cursor.execute('PRAGMA optimize')
-
-def list_all():
- """
- get a list of all the tables and views in the database
- """
- with sqlite3.connect("./data/main.db") as connection:
- cursor = connection.cursor()
- cursor.execute("SELECT name FROM sqlite_master WHERE type IN ('table', 'view');")
- return [table[0] for table in cursor.fetchall()]
-
-def list_views():
- """
- get a list of all the views in the database
- """
- with sqlite3.connect("./data/main.db") as connection:
- cursor = connection.cursor()
- cursor.execute("SELECT name FROM sqlite_master WHERE type='view';")
- return [table[0] for table in cursor.fetchall()]
+++ /dev/null
-import sqlite3
-
-from .utils import list_all
-from .read import read
-
-def write(data):
-
- with sqlite3.connect("./data/main.db") as connection:
- cursor = connection.cursor()
-
- for key in data:
- table, index, feild = key.split("~~")
-
- cursor.execute(f"""
- UPDATE {table}
- SET '{feild}' = '{data[key]}'
- WHERE ROWID = {int(index) + 1};
- """)
-
- connection.commit()
+++ /dev/null
-const outlineStyle: string = "4px solid green";
-const outlineTime: number = 500; // ms
-
-export function setupTrigger() {
- // setup all trigger buttons to send post request on click
- const el: HTMLCollectionOf<Element> =
- document.getElementsByClassName("trigger");
-
- for (let e of el) {
- e.addEventListener("click", (event) => {
- if (event.target instanceof HTMLElement) {
- let time: number = Math.round(Date.now() / 1000);
- let body: FormData = new FormData();
- let delay = document.getElementById(`${event.target.getAttribute('name')}+delay`);
-
- if (delay instanceof HTMLInputElement) {
- body.set(`${event.target.getAttribute('name')}`, `${time}+${delay.value}`);
-
- fetch("", {
- method: "POST",
- body: body
- });
-
- // trigger the button to turn green for approx same time (or 1s)
- let timeout: number = delay.value as unknown as number * 1000;
- if (timeout < outlineTime) { timeout = outlineTime; }
-
- if ( event.target instanceof HTMLElement) {
- event.target.style.outline = outlineStyle;
- setTimeout(function () {
- if ( event.target instanceof HTMLElement) {
- event.target.style.outline = "none"
- }
- }, timeout);
- }
- }
- }
- })
- }
-}
+++ /dev/null
-export function renderDiscount(): void {
- const el: HTMLCollectionOf<Element> =
- document.getElementsByClassName("discount");
-
- for (let e of el) {
- if (e instanceof HTMLElement) {
- if (typeof e.dataset.raw === "string") {
- e.innerHTML = `${e.dataset.raw}%`;
- }
- }
- }
-}
-
-export function renderReverseDiscount(): void {
- const el: HTMLCollectionOf<Element> =
- document.getElementsByClassName("reverseDiscount");
-
- for (let e of el) {
- if (e instanceof HTMLElement) {
- if (typeof e.dataset.raw === "string") {
- e.innerHTML = `${100 - Number(e.dataset.raw)}%`;
- }
- }
- }
-}
+++ /dev/null
-export default function renderPrice(): void {
- const el: HTMLCollectionOf<Element> =
- document.getElementsByClassName("price");
-
- for (let e of el) {
- if (e instanceof HTMLElement) {
- if (typeof e.dataset.raw === "string") {
- e.innerHTML = String(parseFloat(e.dataset.raw).toFixed(2));
- }
- }
- }
-}
+++ /dev/null
-export default function renderShowUntil(): void {
- const el: HTMLCollectionOf<Element> =
- document.getElementsByClassName("showUntil");
-
- for (let e of el) {
- if (e instanceof HTMLElement) {
- if (typeof e.dataset.raw === "string") {
- let duration: number = (eval(e.dataset.raw) * 1000) - Date.now();
- if (duration > 0) {
- e.classList.add("showUntil-showing")
- setTimeout(function () {e.classList.remove("showUntil-showing")}, duration)
- }
- }
- }
- }
-}
+++ /dev/null
-export function setupSounds() {
- const el: HTMLCollectionOf<Element> =
- document.getElementsByClassName("sound");
-
- const context = new AudioContext();
- if (el.length > 0) {
- for (let e of el) {
- if (e instanceof HTMLMediaElement) {
- const track = context.createMediaElementSource(e);
- track.connect(context.destination);
- }
- }
- }
-}
-
-export function playSounds() {
- const el: HTMLCollectionOf<Element> =
- document.getElementsByClassName("sound");
-
- for (let e of el) {
- if (e instanceof HTMLMediaElement) {
- if (typeof e.dataset.raw === "string") {
- if (e.dataset.rawLast != e.dataset.raw) {
- e.play();
- }
- e.dataset.rawLast = e.dataset.raw;
- }
- }
- }
-}
+++ /dev/null
-export default function renderState(): void {
- const el: HTMLCollectionOf<Element> =
- document.getElementsByClassName("state");
-
- for (let e of el) {
- if (e instanceof HTMLElement) {
- if (typeof e.dataset.raw === "string") {
- for (let className of e.classList) {
- if (className.startsWith("state-")) {
- e.classList.remove(className);
- }
- }
-
- e.classList.add(`state-${e.dataset.raw}`);
- }
- }
- }
-}
+++ /dev/null
-export function setupStyle(): void {
- const el: HTMLCollectionOf<Element> =
- document.getElementsByClassName("style");
-
- for (let i = 0; i < el.length; i++) {
- const stylesheet = new CSSStyleSheet();
- document.adoptedStyleSheets.push(stylesheet);
- }
-}
-
-export function renderStyle(): void {
- const el: HTMLCollectionOf<Element> =
- document.getElementsByClassName("style");
-
- for (let [i, e] of Array.from(el).entries()) {
- if (e instanceof HTMLElement) {
- let variable;
- for (let className of e.classList) {
- if (className.includes("--")) {
- variable = className.split("--")[1];
- }
- }
-
- if (typeof e.dataset.raw === "string") {
- document.adoptedStyleSheets[i].replace(`
- :root {--${variable}: ${e.dataset.raw};}
- `);
- }
- }
- }
-}
+++ /dev/null
-export default function renderText(): void {
- const el = document.querySelectorAll(".text,.title");
- for (let e of el) {
- if (e instanceof HTMLElement) {
- if (typeof e.dataset.raw === "string") {
- e.innerHTML = e.dataset.raw;
- }
- }
- }
-}
+++ /dev/null
-import { timeSince, timeUntil, timeFormatBroadcast } from "./utils.js";
-
-const timeUpdate: number = 1000; // ms
-
-export function setupTimeSince(): void {
- const el: HTMLCollectionOf<Element> =
- document.getElementsByClassName("since");
-
- if (el.length > 0) {
- setInterval(
- function () {
- const el: HTMLCollectionOf<Element> =
- document.getElementsByClassName("since");
-
- for (let e of el) {
- if (e instanceof HTMLElement) {
- if (typeof e.dataset.raw === "string") {
- let [hours, minutes, seconds] = timeSince(eval(e.dataset.raw))
- e.innerHTML = timeFormatBroadcast(hours, minutes, seconds);
- }
- }
- }
- },
- timeUpdate
- );
- }
-}
-
-export function setupTimeUntil(): void {
- const el: HTMLCollectionOf<Element> =
- document.getElementsByClassName("until");
-
- if (el.length > 0) {
- setInterval(
- function () {
- const el: HTMLCollectionOf<Element> =
- document.getElementsByClassName("until");
-
- for (let e of el) {
- if (e instanceof HTMLElement) {
- if (typeof e.dataset.raw === "string") {
- let [hours, minutes, seconds] = timeUntil(eval(e.dataset.raw))
- e.innerHTML = timeFormatBroadcast(hours, minutes, seconds);
- }
- }
- }
- },
- timeUpdate
- );
- }
-}
+++ /dev/null
-import { io } from "socket.io-client";
-
-import { renderDiscount, renderReverseDiscount } from "./discount.js";
-import { setupTimeSince, setupTimeUntil } from "./time.js";
-import { setupSounds, playSounds } from "./sound.js";
-import { setupStyle, renderStyle } from "./style.js";
-import renderShowUntil from "./show_until.js";
-import renderState from "./state.js";
-import renderPrice from "./price.js"
-import renderText from "./text.js";
-
-export default function setupUpdate(): void {
- setupTimeSince();
- setupTimeUntil();
- setupSounds();
- setupStyle();
-
- const socket = io();
- socket.on("update", (data) => {
- const start = performance.now()
-
- for (const [key, value] of Object.entries(data)) {
- const el = document.querySelectorAll(`.update[class~='${key}']`);
-
- for (let e of el) {
- if (e instanceof HTMLElement) {
- e.dataset.raw = value as unknown as string;
- }
- }
- }
-
- renderReverseDiscount();
- renderShowUntil();
- renderDiscount();
- renderState();
- renderPrice();
- renderStyle();
- renderText();
- playSounds();
-
- console.log(`Updated in ${performance.now() - start}ms`);
- });
-}
+++ /dev/null
-export function timeUntil(end: number): number[] {
- let current: number = Date.now() / 1000;
- var time: number = end - current;
-
- if (Math.sign(time) == -1) {time = 0;}
-
- let hours: number = Math.floor(time / 3600);
- time %= 3600;
- let minutes: number = Math.floor(time / 60);
- let seconds: number = Math.floor(time % 60);
-
- return [hours, minutes, seconds]
-}
-
-export function timeSince(end: number): number[] {
- let current: number = Date.now() / 1000;
- var time: number = current - end;
-
- if (Math.sign(time) == -1) {time = 0;}
-
- let hours: number = Math.floor(time / 3600);
- time %= 3600;
- let minutes: number = Math.floor(time / 60);
- let seconds: number = Math.floor(time % 60);
-
- return [hours, minutes, seconds]
-}
-
-export function timeFormatBroadcast(hours: number, minutes: number, seconds: number): string {
- var hoursString = hours.toString().padStart(2, "0");
- var minutesString = minutes.toString().padStart(2, "0");
- var secondsString = seconds.toString().padStart(2, "0");
-
- return `${hoursString}h ${minutesString}' ${secondsString}"`
-}
+++ /dev/null
-/* PAGE POSITIONING */
-
-:root {
- --nav-background: lightgray;
-}
-
-body > * {
- padding: 15px;
- overflow: scroll
-}
-nav {
- position: absolute;
- top: 0; bottom: 0; left: 0;
- right: max(80%, calc(100vw - 350px));
- background-color: var(--nav-background);
-}
-main {
- position: absolute;
- top: 0; bottom: 0; right: 0;
- left: min(20%, calc(350px));
-}
-
-/* THE OPTIONS GRID */
-
-.optionsGrid {
- display: grid;
- grid-template-columns: 50px auto auto auto;
-}
-.optionsGrid > * {
- padding: 10px;
- border: 1px solid gray;
- margin-top: -1px;
- margin-right: -1px;
-}
-.optionsGridSpacer { border: none; }
-
-
-:target {
- outline: 4px solid blue;
- z-index: 1;
-}
-.currentValue { color: red; }
-.currentValue[type="radio"] { background-color: red; }
-
-
-/* PRINT MODE
- need to work on this a little bit, currently just hides things that
- shouldnt be printed and thats all. Should be able to potentially display
- extra elements or manipulate the page in a way that make it easy to read
- or annotate on the page.
- */
-
-@media ( max-width: 1000px ) {
- nav { position: static; }
- main { position: static; }
-}
-
-@media print {
- nav, [type="radio"] { display: none }
- [type="submit"] { opacity: 0; }
- main { position: static; }
- .currentValue { color: black; }
- .optionsGrid { grid-template-columns: 0 auto 0 auto; }
- .optionsGrid > * { padding: 0; }
- .optionsGridSpacer { padding: 20px; }
-}
+++ /dev/null
-/*\r
-This file is from the Andika project (https://software.sil.org/andika/).\r
-Copyright (c) 2004-2025 SIL Global (https://www.sil.org/) with Reserved\r
-Font Names "Andika" and "SIL". This Font Software is licensed under the SIL\r
-Open Font License, Version 1.1 (https://openfontlicense.org).\r
-*/\r
-\r
-@font-face {\r
- font-family: "Andika";\r
- font-weight: 400;\r
- src: url(/static/fonts/Andika-Regular.woff2);\r
-}\r
-@font-face {\r
- font-family: "Andika";\r
- font-weight: 500;\r
- src: url(/static/fonts/Andika-Medium.woff2);\r
-}\r
-@font-face {\r
- font-family: "Andika";\r
- font-weight: 600;\r
- src: url(/static/fonts/Andika-SemiBold.woff2);\r
-}\r
-@font-face {\r
- font-family: "Andika";\r
- font-weight: 700;\r
- src: url(/static/fonts/Andika-Bold.woff2);\r
-}\r
-@font-face {\r
- font-family: "Andika";\r
- font-weight: 400;\r
- font-style: italic;\r
- src: url(/static/fonts/Andika-Italic.woff2);\r
-}\r
-@font-face {\r
- font-family: "Andika";\r
- font-weight: 500;\r
- font-style: italic;\r
- src: url(/static/fonts/Andika-MediumItalic.woff2);\r
-}\r
-@font-face {\r
- font-family: "Andika";\r
- font-weight: 600;\r
- font-style: italic;\r
- src: url(/static/fonts/Andika-SemiBoldItalic.woff2);\r
-}\r
-@font-face {\r
- font-family: "Andika";\r
- font-weight: 700;\r
- font-style: italic;\r
- src: url(/static/fonts/Andika-BoldItalic.woff2);\r
-}\r
-\r
-:root {\r
- font-family: "Andika", sans-serif;\r
-}\r
+++ /dev/null
-:root {
- font-family: sans-serif;
- color: white;
- background-color: black;
-}
-
-.infoA { font-size: var(--aFontSize); }
-.infoB { font-size: var(--bFontSize); }
-.infoC { font-size: var(--cFontSize); }
-
-.title { text-transform: capitalize; }
-
-/* INFO A */
-.infoA div > * { padding: 20px; }
-.infoA div {
- position: absolute;
- top: 0; left: 0; bottom: 0; right: 0;
- display: grid;
- grid: none / 1fr;
-}
-
-/* INFO B */
-.infoB div > * { padding: 20px; }
-.infoB div {
- position: absolute;
- top: 0; left: 0; bottom: 0; right: 0;
- display: grid;
- grid: none / 1fr 1fr;
-}
-
-/* INFO C */
-.infoC div > * { padding: 20px; text-align: center; }
-.infoC div {
- position: absolute;
- top: 0; left: 0; bottom: 0; right: 0;
- display: grid;
- grid: none / 6fr 1fr 6fr;
-}
-
+++ /dev/null
-:root {
- font-size: var(--baseFontSize);
- font-weight: 600;
-}
-.title { text-transform: capitalize; }
+++ /dev/null
-body:not(.state-sold, .state-move) .styleBannerBox {
- padding: 0; margin: 0;
-}
-.styleBannerBox {
- opacity: 0; max-height: 0;
- transform: translateX(-20px);
- /* should do some tweeking for the below */
- transition:
- opacity var(--baseAnimationLength),
- max-height var(--baseAnimationLength) cubic-bezier(1,0,0,1),
- padding var(--baseAnimationLength),
- margin var(--baseAnimationLength),
- transform var(--baseAnimationLength);
- overflow: hidden;
-}
-body.state-sold .styleBannerBox,
-body.state-move .styleBannerBox {
- opacity: 1; max-height: 50vh;
- transform: translateX(0px);
-}
+++ /dev/null
-:root {
- --boxHPad: 6px;
- --boxVPad: 2px;
- --boxHMar: 0px;
- --boxVMar: 5px;
-}
-.boxA {
- width: calc(100% - 2 * (var(--boxHMar) + var(--boxHPad) + var(--borderASize)));
-
- color: var(--textAColor);
- text-shadow: var(--textAShadow);
-
- background: var(--backgroundA);
- box-shadow: inset 0 0 var(--innerGlowSize) var(--innerGlowColor);
- border: var(--borderASize) solid var(--borderAColor);
-
- border-radius: var(--borderARadius);
- padding: var(--boxVPad) var(--boxHPad) var(--boxVPad) var(--boxHPad);
- margin: var(--boxVMar) var(--boxHMar) var(--boxVMar) var(--boxHMar);
-}
-.boxA.styleExpand.showUntil:not(.showUntil-showing) {
- margin-bottom: calc(-2 * var(--borderASize));
-}
-
-.boxB {
- width: calc(100% - 2 * (var(--boxHMar) + var(--boxHPad) + var(--borderBSize)));
-
- color: var(--textBColor);
- text-shadow: var(--textBShadow);
-
- background: var(--backgroundB);
- box-shadow: inset 0 0 var(--innerGlowSize) var(--innerGlowColor);
- border: var(--borderBSize) solid var(--borderBColor);
-
- border-radius: var(--borderBRadius);
- padding: var(--boxVPad) var(--boxHPad) var(--boxVPad) var(--boxHPad);
- margin: var(--boxVMar) var(--boxHMar) var(--boxVMar) var(--boxHMar);
-}
-.boxB.styleExpand.showUntil:not(.showUntil-showing) {
- margin-bottom: calc(-2 * var(--borderBSize));
-}
-
-.boxC {
- width: calc(100% - 2 * (var(--boxHMar) + var(--boxHPad) + var(--borderCSize)));
-
- color: var(--textCColor);
- text-shadow: var(--textCShadow);
-
- background: var(--backgroundC);
- box-shadow: inset 0 0 var(--innerGlowSize) var(--innerGlowColor);
- border: var(--borderCSize) solid var(--borderCColor);
-
- border-radius: var(--borderCRadius);
- padding: var(--boxVPad) var(--boxHPad) var(--boxVPad) var(--boxHPad);
- margin: var(--boxVMar) var(--boxHMar) var(--boxVMar) var(--boxHMar);
-}
-.boxC.styleExpand.showUntil:not(.showUntil-showing) {
- margin: 0 0 calc(-2 * var(--borderCSize)) 0;
-}
+++ /dev/null
-.styleCenterShout {
- position: absolute;
- top: 0; left: 0; bottom: 0; right: 0;
-
- display: flex;
- align-items: center; justify-content: center;
-
- text-transform: uppercase;
-}
-.styleCenterShout > div { background-color: red; }
-
-.styleCenterShout.showUntil > div {
- transform: translateX(-100px);
- opacity: 0;
- transition:
- transform var(--baseAnimationLength),
- opacity var(--baseAnimationLength);
-}
-.styleCenterShout.showUntil > div > span {
- display: block;
- transform: translateX(200px);
- transition: transform var(--baseAnimationLength);
-
- text-transform: uppercase;
-}
-.styleCenterShout.showUntil-showing * {
- opacity: 1 !important;
- transform: translateX(0px) !important;
-}
+++ /dev/null
-.styleDiscountBox > em {
- display: block;
- font-size: 0.7em;
- line-height: 1.4em;
-}
-.styleDiscountBox > div {
- font-size: 1.2em;
-}
-
-body:not(.state-discount, .state-move) .styleDiscountBox {
- padding: 0; margin: 0;
-}
-.styleDiscountBox {
- opacity: 0; max-height: 0;
- transform: translateX(-20px);
- /* should do some tweeking for the below */
- transition:
- opacity var(--baseAnimationLength),
- max-height var(--baseAnimationLength) cubic-bezier(1,0,0,1),
- padding var(--baseAnimationLength),
- margin var(--baseAnimationLength),
- transform var(--baseAnimationLength);
- overflow: hidden;
-}
-body.state-discount .styleDiscountBox,
-body.state-move .styleDiscountBox {
- opacity: 1; max-height: 50vh;
- transform: translateX(0px);
-}
-
+++ /dev/null
-body:not(
- .state-product,
- .state-price,
- .state-discount,
- .state-move,
- .state-sold
-) .styleItemBox {
- padding: 0; margin: 0;
-}
-.styleItemBox {
- opacity: 0; max-height: 0;
- transform: translateX(-20px);
- /* should do some tweeking for the below */
- transition:
- opacity var(--baseAnimationLength),
- max-height var(--baseAnimationLength) cubic-bezier(1,0,0,1),
- padding var(--baseAnimationLength),
- margin var(--baseAnimationLength),
- transform var(--baseAnimationLength);
- overflow: hidden;
-}
-body.state-product .styleItemBox,
-body.state-price .styleItemBox,
-body.state-discount .styleItemBox,
-body.state-move .styleItemBox,
-body.state-sold .styleItemBox {
- opacity: 1; max-height: 50vh;
- transform: translateX(0px);
-}
+++ /dev/null
-body:not(
- .state-crawler,
- .state-product
-) .styleLogoBox {
- padding: 0; margin: 0;
-}
-.styleLogoBox {
- opacity: 0; max-height: 0;
- transform: translateX(-20px);
- /* should do some tweeking for the below */
- transition:
- opacity var(--baseAnimationLength),
- max-height var(--baseAnimationLength) cubic-bezier(1,0,0,1),
- padding var(--baseAnimationLength),
- margin var(--baseAnimationLength),
- transform var(--baseAnimationLength);
- overflow: hidden;
-}
-body.state-product .styleLogoBox,
-body.state-crawler .styleLogoBox {
- opacity: 1; max-height: 50vh;
- transform: translateX(0px);
-}
+++ /dev/null
-body:not(.state-price, .state-discount, .state-move) .stylePricingBox {
- padding: 0; margin: 0;
-}
-.stylePricingBox {
- opacity: 0; max-height: 0;
- transform: translateX(-20px);
- /* should do some tweeking for the below */
- transition:
- opacity var(--baseAnimationLength),
- max-height var(--baseAnimationLength) cubic-bezier(1,0,0,1),
- padding var(--baseAnimationLength),
- margin var(--baseAnimationLength),
- transform var(--baseAnimationLength);
- overflow: hidden;
-}
-body.state-price .stylePricingBox,
-body.state-discount .stylePricingBox,
-body.state-move .stylePricingBox {
- opacity: 1; max-height: 50vh;
- transform: translateX(0px);
-}
+++ /dev/null
-.styleQtyBox {
- font-size: 0.7em;
-}
-
-body:not(.state-sold, .state-move) .styleQtyBox {
- padding: 0; margin: 0;
-}
-.styleQtyBox {
- opacity: 0; max-height: 0;
- transform: translateX(-20px);
- /* should do some tweeking for the below */
- transition:
- opacity var(--baseAnimationLength),
- max-height var(--baseAnimationLength) cubic-bezier(1,0,0,1),
- padding var(--baseAnimationLength),
- margin var(--baseAnimationLength),
- transform var(--baseAnimationLength);
- overflow: hidden;
-}
-body.state-sold .styleQtyBox,
-body.state-move .styleQtyBox {
- opacity: 1; max-height: 50vh;
- transform: translateX(0px);
-}
+++ /dev/null
-.styleExpand.showUntil:not(.showUntil-showing) {
- padding: 0; margin: 0;
-}
-.styleExpand.showUntil {
- opacity: 0; max-height: 0;
- transform: translateX(-20px);
- /* should do some tweeking for the below */
- transition:
- opacity var(--baseAnimationLength),
- max-height var(--baseAnimationLength) cubic-bezier(1,0,0,1),
- padding var(--baseAnimationLength),
- margin var(--baseAnimationLength),
- transform var(--baseAnimationLength);
- overflow: hidden;
-}
-.styleExpand.showUntil.showUntil-showing {
- opacity: 1; max-height: 50vh;
- transform: translateX(0px);
-}
+++ /dev/null
-.styleSideBox {
- width: var(--sideBoxWidth);
-
- display: flex;
- flex-direction: column;
-
- margin: 50px;
-}
-.styleSideBox > * {
- margin-bottom: 5px;
- width: 100%;
-}
+++ /dev/null
-.styleSideTimerBox {
- display: inline;
- width: fit-content !important;
-}
+++ /dev/null
-{#
-
- builds the admin page
-
-#}
-<!DOCTYPE html>
-<html>
- <head>
- <link rel="stylesheet" href="/static/admin.css">
-
- <script type="module" defer>
-
-import { setupTrigger } from "/script/admin.js";
-setupTrigger();
-
- </script>
- </head>
- <body>
- <nav>
- <p><b>Tables</b></p>
- <ul style="border: 1px solid gray">
- {% for table in indexes %}
- {% set table_name = table.split("--")[1] %}
- <li><a href="/admin/{{ table_name }}">{{ table_name }}</a></li>
- {% endfor %}
- </ul>
- </nav>
- <main>
- <h1>Admin</h1>
-
- <form method="GET" action="/db">
- <button type="submit">Donwload database</button>
- </form>
- <form method="POST" action="/db">
- <input type="file"/>
- <button type="submit">Upload database</button>
- </form>
-
- <ul>
- <li><a href="/">Main GFX overlay</a></li>
- <li><a href="/info/a">Info page A</a> <em>(Item name, description, Anchor notes)</em></li>
- <li><a href="/info/b">Info page B</a> <em>(Price, discount and stock)</em></li>
- <li><a href="/info/c">Info page C</a> <em>(Gallery screen, timing and state)</em></li>
- </ul>
- </main>
- </body>
-</html>
+++ /dev/null
-<label>{{ label }}</label>
-<span>
- <input id="{{ name }}" type="color" name="{{ name }}" value="{{ value }}" />
- {{ value }}
-</span>
-<span class="currentValue" style="background-color: {{ value }};"></span>
+++ /dev/null
-<label>{{ label }}</label>
-<input id="{{ name }}" type="number" name="{{ name }}" value="{{ value }}" step="0.01" />
-<span class="currentValue">{{ value }}</span>
+++ /dev/null
-<label>{{ label }}</label>
-<input id="{{ name }}" type="number" name="{{ name }}" value="{{ value }}" />
-<span class="currentValue">{{ value }}</span>
+++ /dev/null
-<label>{{ label }}</label>
-<input id="{{ name }}" type="text" name="{{ name }}" value="{{ value }}" />
-<span class="currentValue">{{ value }}</span>
-
+++ /dev/null
-<label>{{ label }}</label>
-<textarea id="{{ name }}" style="grid-column: 3 / span 2;" name="{{ name }}">
- {{- value -}}
-</textarea>
+++ /dev/null
-<span id="{{ name }}" style="grid-column: 2 / span 3;">
- <noscript style="color: red;">Trigger will not function without JS<br></noscript>
- <button type="button" name="{{ name }}" value="{{ label }}" class="trigger">
- {{ label }}
- </button>
- <input type="number" id="{{ name }}+delay" value="{{ value.split('+')[1] }}"/><span> seconds delay <em>(This does not update the triggered data.)</em></span>
-</span>
+++ /dev/null
-<!doctype html>
-<html>
- <head>
- <link rel="stylesheet" href="/static/style.css">
- <link rel="stylesheet" href="/static/andika.css">
-
- {# ELEMENT STYLING IMPORTS #}
- <link rel="stylesheet" href="/static/style/side_box.css">
- <link rel="stylesheet" href="/static/style/qty_box.css">
- <link rel="stylesheet" href="/static/style/side_timer_box.css">
- <link rel="stylesheet" href="/static/style/center_shout.css">
- <link rel="stylesheet" href="/static/style/discount_box.css">
- <link rel="stylesheet" href="/static/style/banner_box.css">
- <link rel="stylesheet" href="/static/style/pricing_box.css">
- <link rel="stylesheet" href="/static/style/item_box.css">
- <link rel="stylesheet" href="/static/style/logo_box.css">
-
- {# HELPER IMPORTS #}
- <link rel="stylesheet" href="/static/style/box.css">
- <link rel="stylesheet" href="/static/style/show_until.css">
- <link rel="stylesheet" href="/static/style/background.css">
-
- <script type="importmap">
- {
- "imports": {
- "socket.io-client": "https://cdn.socket.io/4.8.3/socket.io.esm.min.js"
- }
- }
- </script>
- <script type="module" defer>
-
-import setupUpdate from "/script/update.js";
-setupUpdate();
-
- </script>
- </head>
- <body class="update data__state~~text--state state">
-
- {# GLOBAL STYLING ELEMENTS #}
- <span class="update style~~text--baseAnimationLength style"></span>
- <span class="update style~~text--sideBoxWidth style"></span>
- <span class="update style~~text--baseFontSize style"></span>
-
- {# DYNAMIC STYLING ELEMENTS #}
-
- <span class="update style__colorscheme~~text--borderASize style"></span>
- <span class="update style__colorscheme~~color--borderAColor style"></span>
- <span class="update style__colorscheme~~text--borderBSize style"></span>
- <span class="update style__colorscheme~~color--borderBColor style"></span>
- <span class="update style__colorscheme~~text--borderCSize style"></span>
- <span class="update style__colorscheme~~color--borderCColor style"></span>
-
- <span class="update style__colorscheme~~color--textAColor style"></span>
- <span class="update style__colorscheme~~color--textBColor style"></span>
- <span class="update style__colorscheme~~color--textCColor style"></span>
- <span class="update style__colorscheme~~text--textAShadow style"></span>
- <span class="update style__colorscheme~~text--textBShadow style"></span>
- <span class="update style__colorscheme~~text--textCShadow style"></span>
-
- <span class="update style__colorscheme~~text--borderARadius style"></span>
- <span class="update style__colorscheme~~text--borderBRadius style"></span>
- <span class="update style__colorscheme~~text--borderCRadius style"></span>
-
- <span class="update style__colorscheme__backgroundA~~text--backgroundA style"></span>
- <span class="update style__colorscheme__backgroundB~~text--backgroundB style"></span>
- <span class="update style__colorscheme__backgroundC~~text--backgroundC style"></span>
-
- <span class="update style__colorscheme~~color--innerGlowColor style"></span>
- <span class="update style__colorscheme~~text--innerGlowSize style"></span>
-
- {# SFX ELEMENTS #}
- <audio src="/static/media/sfx/banner_shout.wav" class="update data~~trigger--bannerShoutTrigger sound"></audio>
- <audio src="/static/media/sfx/clock.wav" class="update data__clock~~number--positionDegrees sound"></audio>
- <audio src="/static/media/sfx/state_change.wav" class="update data__state~~text--state sound"></audio>
- <audio src="/static/media/sfx/shout.wav" class="update data~~trigger--centerShoutTrigger sound"></audio>
-
- {# CENTER SHOUT #}
- <div class="styleCenterShout update data~~trigger--centerShoutTrigger showUntil">
- <div>
- <span class="update data~~text--centerShoutText text"></span>
- </div>
- </div>
-
- {# SIDE BOX
- includes (top to bottom):
- - LOGO BOX (show during: crawler, product)
- - BANNER BOX (show during: move, sold)
- - ITEM BOX (show during: product, price, discount, move, sold)
- - PRICING BOX (show during: price, discount, move)
- - DISCOUNT BOX (show during: discount, move)
- - BANNER SHOUT BOX (show AD)
- - QTY BOX (show during: move, sold)
- - TIMER 1 BOX (show AD)
- - TIMER 2 BOX (show AD)
-
- these are switched on and off using the state class from the
- body tag.
- #}
- <div class="styleSideBox">
-
- {# LOGO BOX #}
- <div class="styleLogoBox">
- LOGO BOX
- </div>
-
- {# BANNER BOX #}
- <div class="styleBannerBox boxC">
- BANNER BOX
- </div>
-
- {# ITEM BOX #}
- <div class="styleItemBox boxB">
- <div class="boxA">
- ITEM BOX
- </div>
- </div>
-
- {# PRICING BOX #}
- <div class="stylePricingBox boxA">
- PRICING BOX
- </div>
-
- {# DISOUNT BOX #}
- <div class="styleDiscountBox boxB">
- <em>Now only...</em>
- <div class="boxA">
- <b><span class="update data__product~~number--discount discount"></span></b>
- OFF!
- </div>
- </div>
-
- {# BANNER SHOUT BOX #}
- <div class="styleExpand boxC update data~~trigger--bannerShoutTrigger showUntil">
- <span class="update data~~text--bannerShoutText title"></span>
- </div>
-
- {# QTY BOX #}
- <div class="styleQtyBox boxB">
- <em>Sold:
- <span class="update data__product~~number--quantityTotal text"></span>...</em>
- only
- <span class="update data__product~~number--quantityCurrent text"></span>
- LEFT
- </div>
-
- {# TIMER BOX #}
- <div class="styleSideTimerBox boxA">
- TIMER 1!
- </div>
- <div class="styleSideTimerBox boxA">
- TIMER 2!
- </div>
- </div>
- </body>
-</html>
+++ /dev/null
-<!doctype html>
-<html>
- <head>
- <link rel="stylesheet" href="/static/info.css">
- <script type="importmap">
- {
- "imports": {
- "socket.io-client": "https://cdn.socket.io/4.8.3/socket.io.esm.min.js"
- }
- }
- </script>
- <script type="module" defer>
-
-import setupUpdate from "/script/update.js";
-setupUpdate();
-
- </script>
- </head>
- <body class="infoA">
- <span class="update style~~text--aFontSize style"></span>
-
- <div>
- <span>
- <u style="color: pink;"><span class="update data__product~~text--name text"></span></u><br>
- <span style="color: pink" class="update data__product~~text--description text"></span>
- </span>
-
- <span class="styleProductNotes update data__product~~textarea--notes text"></span>
-
- <span style="color: yellow" class="update data~~textarea--anchorNotes text"></span>
- </div>
- </body>
-</html>
+++ /dev/null
-<!doctype html>
-<html>
- <head>
- <link rel="stylesheet" href="/static/info.css">
- <script type="importmap">
- {
- "imports": {
- "socket.io-client": "https://cdn.socket.io/4.8.3/socket.io.esm.min.js"
- }
- }
- </script>
- <script type="module" defer>
-
-import setupUpdate from "/script/update.js";
-setupUpdate();
-
- </script>
- </head>
- <body class="infoB">
- <span class="update style~~text--bFontSize style"></span>
-
- <div>
- <span>
- <span style="background-color: green" class="update data__product__currentPriceString~~text--currentPriceString text"></span> <em>(was <span class="update data__product__originalPriceString~~text--originalPriceString text"></span>)</em>
- </span>
-
- <span>
- <span class="update data__product~~number--quantityCurrent text"></span>
- left of
- <span class="update data__product~~number--quantityTotal text"></span>
- total
- </span>
-
- <span>
- <span style="background-color: purple;" class="update data__product~~number--discount discount"></span> off
- </span>
-
- <span>
- <span style="background-color: purple;" class="update data__product~~number--discount reverseDiscount"></span> full price
- </span>
-
- <span style="grid-column: 1 / span 2;">
- Target: <span style="background-color: orange;" class="update data__product~~number--targetDiscount discount"></span> (<span class="update data__product__remainingDiscount~~number--remainingDiscount discount"></span> left to drop)
- </span>
-
- <span style="grid-column: 1 / span 2;">
- Max: <span style="background-color: red;" class="update data__product~~number--maximumDiscount discount"></span> (<span class="update data__product__remainingMaximumDiscount~~number--remainingMaximumDiscount discount"></span> left to drop)
- </span>
- </div>
- </body>
-</html>
+++ /dev/null
-<!doctype html>
-<html>
- <head>
- <link rel="stylesheet" href="/static/info.css">
- <script type="importmap">
- {
- "imports": {
- "socket.io-client": "https://cdn.socket.io/4.8.3/socket.io.esm.min.js"
- }
- }
- </script>
- <script type="module" defer>
-
-import setupUpdate from "/script/update.js";
-setupUpdate();
-
- </script>
- </head>
- <body class="infoC">
- <span class="update style~~text--cFontSize style"></span>
-
- <div>
- <span>Current product:<br><span style="background-color: blue;" class="update data__product~~text--name text"></span></span>
- <span>→</span>
- <span>Next product:<br><span style="background-color: blue;" class="update data__product__next~~text--name text"></span></span>
-
- <span>Current state: <span class="update data__state~~text--state title"></span></span>
- <span>→</span>
- <span>Next state: <span class="update data__state~~text--nextState title"></span></span>
-
- <span style="grid-column: 1 / span 3; font-size: 2em; font-size: 2em;">
- <span class="update data~~trigger--startTrigger since"></span> <span style="background-color: red;">T-<span class="update data__endTime~~text--endTime until"></span></span>
- </span>
- </div>
- </body>
-</html>
+++ /dev/null
-{#
-
- builds the admin page
-
-#}
-<!DOCTYPE html>
-<html>
- <head>
- <link rel="stylesheet" href="/static/admin.css">
-
- <script type="module" defer>
-
-import { setupTrigger } from "/script/admin.js";
-setupTrigger();
-
- </script>
- </head>
- <body>
- <nav>
- <p><a href="/admin">.. back</a></p>
- <p><b>{{ table_name }}</b></p>
- <ol>
- {% for row in table %}
- <li style="border: 1px solid gray; margin-top: -1px;"><ul>
- {% set group = loop.index0 %}
- {% for key in row %}
- <li><a href="#{{ table_name ~ "~~" ~ group ~ "~~" ~ key }}">{{ key }}</a></li>
- {% endfor %}
- </ul></li>
- {% endfor %}
- </ol>
- </nav>
- <main>
- <h1>Table: {{ table_name }}</h1>
- <form class="optionsGrid" method="post">
- <input type="submit" value="Submit table" style="grid-column: 1 / span 4;">
- <span class="optionsGridSpacer" style="grid-column: 1 / span 4;"></span>
- {% for row in table %}
-
- {# CREATE RADIO BUTTON FOR ROW #}
-
- <span style="
- grid-row: {{ ((row|length + 1) * loop.index0) + 3 }} / span {{ row|length }};
- display: flex;
- justify-content: center;
- align-items: center;
- ">
- {% if loop.index0 == indexes["int--" ~ table_name] %}
- <input type="radio" name="{{ "indexes~~0~~int--" ~ table_name }}" value="{{ loop.index0 }}" checked="checked" class="currentValue">
- {% else %}
- <input type="radio" name="{{ "indexes~~0~~int--" ~ table_name }}" value="{{ loop.index0 }}" >
- {% endif %}
- </span>
-
- {# CREATE FEILDS #}
-
- {% set group = loop.index0 %}
- {% for feild in row %}
-
- {# SELECT ELEMENT FROM TYPE #}
-
- {% set type, label = feild.split('--') %}
- {% set value = row[feild] %}
- {% set name = table_name ~ "~~" ~ group ~ "~~" ~ feild %}
-
- {% if type == "textarea" %}
- {% include "admin/textarea.html" %}
- {% elif type == "text" %}
- {% include "admin/text.html" %}
- {% elif type == "number" %}
- {% include "admin/number.html" %}
- {% elif type == "float" %}
- {% include "admin/float.html" %}
- {% elif type == "trigger" %}
- {% include "admin/trigger.html" %}
- {% elif type == "color" %}
- {% include "admin/color.html" %}
- {% endif %}
- {% endfor %}
- <span class="optionsGridSpacer" style="grid-column: 1 / span 4;"></span>
-
- {% endfor %}
- <input type="submit" value="Submit table" style="grid-column: 1 / span 4;">
- </form>
-
- <code>
- Note: Tables are rendered statically, it is not safe to opperate the same interface in two places at once.
- <br>
- Processed {{ table|length * table[0]|length }} feilds in
- {% if (time_taken * 1000)|round(2) > 20 %}
- <span style="color: red;">{{ (time_taken * 1000)|round(2) }}ms</span>
- {% else %}
- {{ (time_taken * 1000)|round(2) }}ms
- {% endif %}
- </code>
- </main>
- </body>
-</html>
+++ /dev/null
-def deindex(data, indexes):
- deindexed = {}
- for key in data:
- table, index, feild = key.split("~~")
- if table != "indexes" and int(index) == indexes[f"int--{table}"]:
- deindexed.update({
- f"{table}~~{feild}": data[key]
- })
-
- return deindexed
{
- "include": ["./director/src","./director/static"],
+ "include": ["./argo/src","./argo/static"],
"compilerOptions": {
- "rootDir": "./director/src",
- "outDir": "./director/build",
+ "rootDir": "./argo/src",
+ "outDir": "./argo/build",
"module": "esnext",
"target": "esnext",