diff --git a/twitch/chatguessr.js b/twitch/chatguessr.js new file mode 100644 index 0000000..3f5f9a4 --- /dev/null +++ b/twitch/chatguessr.js @@ -0,0 +1,69 @@ +(function() { + var isOpen = false, + guesses = {}; + + function DBsetOpen(open) { + $.inidb.SetBoolean("chatguessr", "isOpen", "", open); + isOpen = open; + sendData('status',open); + } + + function DBClearG() { + guesses = {}; + $.inidb.SetString("chatguessr", "guesses", JSON.stringify(guesses)); + sendData('guesses',guesses); + } + + function DBAddG(user,position) { + if(guesses[user] && guesses[user].length > 0){ + }else{ + guesses[user] = position; + $.inidb.SetString("chatguessr", "guesses", JSON.stringify(guesses)); + sendData('guesses',guesses); + } + } + + function sendData(tpe, data) { + $.panelsocketserver.sendJSONToAll(JSON.stringify({ + 'eventFamily': 'chatguessr', + 'eventType': tpe, + 'data': data + })); + } + + $.bind('command', function(event) { + + const sender = "" + event.getSender().toLowerCase(), + command = event.getCommand(), + args = event.getArgs(), + action = args[0]; + + if (command.equalsIgnoreCase('g')) { + if(isOpen) DBAddG(sender,args); + }else if (command.equalsIgnoreCase('cg')) { + if (!action) { + $.say($.whisperPrefix(sender) + $.lang.get('chatguessr.help', ' Use "!cg [open | close | ...]" to open/close the feature.')); + } else if (action.equalsIgnoreCase('open')) { + if(!isOpen) DBsetOpen(true); + } else if (action.equalsIgnoreCase('close')) { + if(isOpen) DBsetOpen(false); + } else if (action.equalsIgnoreCase('clear')) { + DBClearG(); + } else if (action.equalsIgnoreCase('timer')) { + sendData(action, new Date(Date.now().getTime()+value*1000*60)); + } else { + $.say($.whisperPrefix(sender) + $.lang.get('chatguessr.help')); + } + } + }); + + $.bind('initReady', function() { + $.registerChatCommand('./custom/custom/chatguessr.js', 'cg'); + $.registerChatCommand('./custom/custom/chatguessr.js', 'g',7); + + $.registerChatSubcommand('cg', 'open', 2); + $.registerChatSubcommand('cg', 'close', 2); + $.registerChatSubcommand('cg', 'clear', 2); + }); + +})(); \ No newline at end of file diff --git a/web/chatguessr/SRC_CG.js b/web/chatguessr/SRC_CG.js new file mode 100644 index 0000000..0b50d24 --- /dev/null +++ b/web/chatguessr/SRC_CG.js @@ -0,0 +1,1730 @@ +let deps = { + "axios": "^0.22.0", + "codegrid-js": "git+https://github.com/Tzhf/codegrid-js.git", + "dotenv": "^8.2.0", + "electron-store": "^8.0.1", + "electron-updater": "^4.3.9", + "jquery": "^3.5.1", + "tmi.js": "^1.8.5" + } + +const Game = require("./Classes/Game"); +const GameHelper = require("./utils/GameHelper"); + +const tmi = require("./Classes/tmi"); + +const game = new Game(); + +class GameHandler { + constructor(win, settingsWindow) { + this.win = win; + this.settingsWindow = settingsWindow; + this.initTmi(); + this.init(); + } + + init = () => { + game.init(this.win, settings); + + // Browser Listening + this.win.webContents.on("did-navigate-in-page", (e, url) => { + if (GameHelper.isGameURL(url)) { + game.start(url, settings.isMultiGuess).then(() => { + this.win.webContents.send("game-started", game.isMultiGuess); + TMI.action(`${game.round == 1 ? "🌎 A new seed of " + game.mapName : "🌎 Round " + game.round} has started`); + openGuesses(); + }); + } else { + game.outGame(); + this.win.webContents.send("game-quitted"); + } + }); + + this.win.webContents.on("did-frame-finish-load", () => { + if (!game.isInGame) return; + this.win.webContents.send("refreshed-in-game", settings.noCompass); + // Checks and update seed when the game has refreshed + // update the current location if it was skipped + // if the streamer has guessed returns scores + game.refreshSeed().then((scores) => { + if (scores) showResults(scores.location, scores.scores); + }); + + this.win.webContents.executeJavaScript(` + window.nextRoundBtn = document.querySelector('[data-qa="close-round-result"]'); + if(window.nextRoundBtn) { + nextRoundBtn.addEventListener("click", () => { + nextRoundBtn.setAttribute('disabled', 'disabled'); + ipcRenderer.send('next-round-click'); + }); + } + `); + }); + + const showResults = (location, scores) => { + const round = game.seed.state === "finished" ? game.round : game.round - 1; + this.win.webContents.send("show-round-results", round, location, scores); + TMI.action(`🌎 Round ${round} has finished. Congrats ${GameHelper.toEmojiFlag(scores[0].flag)} ${scores[0].username} !`); + }; + + ipcMain.on("next-round-click", () => nextRound()); + + const nextRound = () => { + game.nextRound(); + if (game.seed.state === "finished") { + processTotalScores(); + } else { + this.win.webContents.send("next-round", game.isMultiGuess); + TMI.action(`🌎 Round ${game.round} has started`); + openGuesses(); + } + }; + + TMI.client.on("guess", async (from, userstate, message, self) => { + const msg = message.split("!g")[1].trim(); + if (!GameHelper.isCoordinates(msg)) return; + + const location = { lat: parseFloat(msg.split(",")[0]), lng: parseFloat(msg.split(",")[1]) }; + game.handleUserGuess(userstate, location).then((res) => { + if (res === "alreadyGuessed") return TMI.say(`${userstate["display-name"]} you already guessed`); + const { user, guess } = res; + this.win.webContents.send("render-guess", guess, game.nbGuesses); + if (settings.showHasGuessed) return TMI.say(`${GameHelper.toEmojiFlag(user.flag)} ${userstate["display-name"]} guessed`); + + }); + }); +} + +module.exports = GameHandler; + +const GameHandler = require("./GameHandler"); + +const Scoreboard = require("./Classes/Scoreboard"); +const Store = require("./utils/Store"); + +window.addEventListener("DOMContentLoaded", () => { + window.ipcRenderer = require("electron").ipcRenderer; + window.$ = window.jQuery = require("jquery"); + window.MAP = null; + + hijackMap(); + + const head = document.getElementsByTagName("head")[0]; + + + + const scoreboardContainer = document.createElement("div"); + scoreboardContainer.setAttribute("id", "scoreboardContainer"); + scoreboardContainer.innerHTML = ` +
+
+ + GUESSES (0) + +
+ + + + + + + + + + + +
#PlayerStreakDistanceScore
+
`; + document.body.appendChild(scoreboardContainer); + + + + const init = () => { + const markerRemover = document.createElement("style"); + markerRemover.innerHTML = ".map-pin{display:none}"; + + const settingsIcon = document.createElement("div"); + settingsIcon.setAttribute("title", "Settings (ctrl+p)"); + settingsIcon.id = "settingsIcon"; + settingsIcon.innerHTML = "⚙️"; + settingsIcon.addEventListener("click", () => { + ipcRenderer.send("openSettings"); + }); + document.body.appendChild(settingsIcon); + + const scoreboard = new Scoreboard(); + + const showScoreboard = document.createElement("div"); + showScoreboard.setAttribute("title", "Show scoreboard"); + showScoreboard.id = "showScoreboard"; + showScoreboard.innerHTML = "👁️‍🗨️"; + showScoreboard.addEventListener("click", () => { + scoreboard.setVisibility(); + }); + + ipcRenderer.on("game-started", (e, isMultiGuess) => { + document.body.appendChild(showScoreboard); + scoreboard.checkVisibility(); + scoreboard.reset(isMultiGuess); + }); + + ipcRenderer.on("refreshed-in-game", (e, noCompass) => { + document.body.appendChild(showScoreboard); + scoreboard.checkVisibility(); + drParseNoCompass(noCompass); + }); + + ipcRenderer.on("game-quitted", () => { + scoreboard.hide(); + if ($("#showScoreboard")) $("#showScoreboard").remove(); + markerRemover.remove(); + clearMarkers(); + }); + + ipcRenderer.on("render-guess", (e, guess, nbGuesses) => { + scoreboard.setTitle(`GUESSES (${nbGuesses})`); + scoreboard.renderGuess(guess); + }); + + ipcRenderer.on("render-multiguess", (e, guesses, nbGuesses) => { + scoreboard.setTitle(`GUESSES (${nbGuesses})`); + scoreboard.renderMultiGuess(guesses); + }); + + ipcRenderer.on("pre-round-results", () => document.body.appendChild(markerRemover)); + + ipcRenderer.on("show-round-results", (e, round, location, scores) => { + scoreboard.show(); + scoreboard.setTitle(`ROUND ${round} RESULTS`); + scoreboard.displayScores(scores); + scoreboard.showSwitch(false); + populateMap(location, scores); + }); + + ipcRenderer.on("show-final-results", (e, totalScores) => { + document.body.appendChild(markerRemover); + scoreboard.show(); + scoreboard.setTitle("HIGHSCORES"); + scoreboard.showSwitch(false); + scoreboard.displayScores(totalScores, true); + clearMarkers(); + }); + + ipcRenderer.on("next-round", (e, isMultiGuess) => { + scoreboard.checkVisibility(); + scoreboard.reset(isMultiGuess); + scoreboard.showSwitch(true); + setTimeout(() => { + markerRemover.remove(); + clearMarkers(); + }, 1000); + }); + + ipcRenderer.on("switch-on", () => scoreboard.switchOn(true)); + ipcRenderer.on("switch-off", () => scoreboard.switchOn(false)); + + ipcRenderer.on("game-settings-change", (e, noCompass) => drParseNoCompass(noCompass)); + }; +}); + +let markers = []; +let polylines = []; +function populateMap(location, scores) { + const infowindow = new google.maps.InfoWindow(); + const icon = { + path: `M13.04,41.77c-0.11-1.29-0.35-3.2-0.99-5.42c-0.91-3.17-4.74-9.54-5.49-10.79c-3.64-6.1-5.46-9.21-5.45-12.07 + c0.03-4.57,2.77-7.72,3.21-8.22c0.52-0.58,4.12-4.47,9.8-4.17c4.73,0.24,7.67,3.23,8.45,4.07c0.47,0.51,3.22,3.61,3.31,8.11 + c0.06,3.01-1.89,6.26-5.78,12.77c-0.18,0.3-4.15,6.95-5.1,10.26c-0.64,2.24-0.89,4.17-1,5.48C13.68,41.78,13.36,41.78,13.04,41.77z + `, + fillColor: "#de3e3e", + fillOpacity: 0.7, + scale: 1.2, + strokeColor: "#000000", + strokeWeight: 1, + anchor: new google.maps.Point(14, 43), + labelOrigin: new google.maps.Point(13.5, 15), + }; + + const locationMarker = new google.maps.Marker({ + position: location, + url: `http://maps.google.com/maps?q=&layer=c&cbll=${location.lat},${location.lng}`, + icon: icon, + map: MAP, + }); + google.maps.event.addListener(locationMarker, "click", () => { + window.open(locationMarker.url, "_blank"); + }); + markers.push(locationMarker); + + icon.scale = 1; + scores.forEach((score, index) => { + const color = index == 0 ? "#E3BB39" : index == 1 ? "#C9C9C9" : index == 2 ? "#A3682E" : score.color; + icon.fillColor = color; + + const guessMarker = new google.maps.Marker({ + position: score.position, + icon: icon, + map: MAP, + label: { color: "#000", fontWeight: "bold", fontSize: "16px", text: `${index + 1}` }, + }); + google.maps.event.addListener(guessMarker, "mouseover", () => { + infowindow.setContent(` +

+ ${score.flag ? `` : ""}${score.username}
+ ${score.distance >= 1 ? parseFloat(score.distance.toFixed(1)) + "km" : parseInt(score.distance * 1000) + "m"}
+ ${score.score} +

+ `); + infowindow.open(MAP, guessMarker); + }); + google.maps.event.addListener(guessMarker, "mouseout", () => { + infowindow.close(); + }); + markers.push(guessMarker); + + polylines.push( + new google.maps.Polyline({ + strokeColor: color, + strokeWeight: 4, + strokeOpacity: 0.6, + geodesic: true, + map: MAP, + path: [score.position, location], + }) + ); + }); +} + +function clearMarkers() { + while (markers[0]) { + markers.pop().setMap(null); + } + while (polylines[0]) { + polylines.pop().setMap(null); + } +} + +function hijackMap() { + const MAPS_API_URL = "https://maps.googleapis.com/maps/api/js?"; + const GOOGLE_MAPS_PROMISE = new Promise((resolve, reject) => { + let scriptObserver = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node.tagName === "SCRIPT" && node.src.startsWith(MAPS_API_URL)) { + node.onload = () => { + scriptObserver.disconnect(); + scriptObserver = undefined; + resolve(); + }; + } + } + } + }); + + let bodyDone = false; + let headDone = false; + + new MutationObserver((_, observer) => { + if (!bodyDone && document.body) { + if (scriptObserver) { + scriptObserver.observe(document.body, { + childList: true, + }); + bodyDone = true; + } + } + if (!headDone && document.head) { + if (scriptObserver) { + scriptObserver.observe(document.head, { + childList: true, + }); + headDone = true; + } + } + if (headDone && bodyDone) { + observer.disconnect(); + } + }).observe(document.documentElement, { + childList: true, + subtree: true, + }); + }); + + function runAsClient(f) { + const script = document.createElement("script"); + script.type = "text/javascript"; + script.text = `(${f.toString()})()`; + document.body.appendChild(script); + } + + GOOGLE_MAPS_PROMISE.then(() => { + runAsClient(() => { + const google = window.google; + const isGamePage = () => location.pathname.startsWith("/results/") || location.pathname.startsWith("/game/"); + const onMapUpdate = (map) => { + try { + if (!isGamePage()) return; + MAP = map; + } catch (error) { + console.error("GeoguessrHijackMap Error:", error); + } + }; + + const oldMap = google.maps.Map; + google.maps.Map = Object.assign( + function (...args) { + const res = oldMap.apply(this, args); + this.addListener("idle", () => { + if (MAP != null) return; + onMapUpdate(this); + }); + return res; + }, + { + prototype: Object.create(oldMap.prototype), + } + ); + }); + }); +} + +function drParseNoCompass(noCompass) { + const style = document.getElementById("noCompass"); + if (noCompass) { + if (!style) { + const style = document.createElement("style"); + style.id = "noCompass"; + style.innerHTML = ".compass { display: none }.game-layout__compass{display: none}"; + document.head.appendChild(style); + } + } else { + if (style) style.remove(); + } +} + +function drParseNoCar() { + if (!noCar) return; + + const OPTIONS = { colorR: 0.5, colorG: 0.5, colorB: 0.5 }; + const vertexOld = + "const float f=3.1415926;varying vec3 a;uniform vec4 b;attribute vec3 c;attribute vec2 d;uniform mat4 e;void main(){vec4 g=vec4(c,1);gl_Position=e*g;a=vec3(d.xy*b.xy+b.zw,1);a*=length(c);}"; + const fragOld = + "precision highp float;const float h=3.1415926;varying vec3 a;uniform vec4 b;uniform float f;uniform sampler2D g;void main(){vec4 i=vec4(texture2DProj(g,a).rgb,f);gl_FragColor=i;}"; + const vertexNew = ` + const float f=3.1415926; + varying vec3 a; + varying vec3 potato; + uniform vec4 b; + attribute vec3 c; + attribute vec2 d; + uniform mat4 e; + void main(){ + vec4 g=vec4(c,1); + gl_Position=e*g; + a = vec3(d.xy * b.xy + b.zw,1); + a *= length(c); + potato = vec3(d.xy, 1.0) * length(c); + } + `; + + const fragNew = ` + precision highp float; + const float h=3.1415926; + varying vec3 a; + varying vec3 potato; + uniform vec4 b; + uniform float f; + uniform sampler2D g; + void main(){ + + vec2 aD = potato.xy / a.z; + float thetaD = aD.y; + + float thresholdD1 = 0.6; + float thresholdD2 = 0.7; + + float x = aD.x; + float y = abs(4.0*x - 2.0); + float phiD = smoothstep(0.0, 1.0, y > 1.0 ? 2.0 - y : y); + + vec4 i = vec4(thetaD > mix(thresholdD1, thresholdD2, phiD) + ? vec3(float(${OPTIONS.colorR}), float(${OPTIONS.colorG}), float(${OPTIONS.colorB})) // texture2DProj(g,a).rgb * 0.25 + : texture2DProj(g,a).rgb,f); + gl_FragColor=i; + } + `; + + function installShaderSource(ctx) { + const g = ctx.shaderSource; + function shaderSource() { + if (typeof arguments[1] === "string") { + let glsl = arguments[1]; + if (glsl === vertexOld) glsl = vertexNew; + else if (glsl === fragOld) glsl = fragNew; + return g.call(this, arguments[0], glsl); + } + return g.apply(this, arguments); + } + shaderSource.bestcity = "bintulu"; + ctx.shaderSource = shaderSource; + } + + function installGetContext(el) { + const g = el.getContext; + el.getContext = function () { + if (arguments[0] === "webgl" || arguments[0] === "webgl2") { + const ctx = g.apply(this, arguments); + if (ctx && ctx.shaderSource && ctx.shaderSource.bestcity !== "bintulu") { + installShaderSource(ctx); + } + return ctx; + } + return g.apply(this, arguments); + }; + } + + const f = document.createElement; + document.createElement = function () { + if (arguments[0] === "canvas" || arguments[0] === "CANVAS") { + const el = f.apply(this, arguments); + installGetContext(el); + return el; + } + return f.apply(this, arguments); + }; +} +const path = require("path"); +require("dotenv").config({ path: path.join(__dirname, "../../.env") }); + +const axios = require("axios"); + +const CG = require("codegrid-js").CodeGrid(); +const countryCodes = require("./countryCodes"); +const countryCodesNames = require("./countryCodesNames"); + +class GameHelper { + /** + * Checks if '/game/' is in the URL + * @param {string} url Game URL + * @return {boolean} + */ + static isGameURL = (url) => url.includes("/game/"); + + /** + * Gets the Game ID from a game URL + * Checks if ID is 16 characters in length + * @param {string} url Game URL + * @return {string|boolean} id or false + */ + static getGameId = (url) => { + const id = url.substring(url.lastIndexOf("/") + 1); + if (id.length == 16) { + return id; + } else { + return false; + } + }; + + /** + * Fetch a game seed + * @param {string} url + * @return {Promise} Seed Promise + */ + static fetchSeed = async (url) => { + return axios + .get(`https://www.geoguessr.com/api/v3/games/${url.substring(url.lastIndexOf("/") + 1)}`) + .then((res) => res.data) + .catch((error) => console.log(error)); + }; + + /** + * Returns a country code + * @param {Object} location {lat, lng} + * @return {Promise} Country code Promise + */ + static getCountryCode = async (location) => { + return axios + .get(`https://api.bigdatacloud.net/data/reverse-geocode?latitude=${location.lat}&longitude=${location.lng}&key=${process.env.BDC_KEY}`) + .then((res) => countryCodes[res.data.countryCode]) + .catch((error) => { + // if BDC returns an error use CodeGrid + return new Promise((resolve, reject) => { + CG.getCode(location.lat, location.lng, (error, code) => { + resolve(code); + reject(new Error(error)); + }); + }).then((code) => countryCodes[code.toUpperCase()]); + }); + }; + + /** + * Returns a country code + * It uses CodeGrid first and then BDC if needed + * @param {Object} location {lat, lng} + * @return {Promise} Country code Promise + */ + static getCountryCodeLocally = async (location) => { + return new Promise((resolve, reject) => { + let coordinates = this.getSurroundings(location); + let promises = []; + coordinates.forEach((coord) => { + promises.push(this.getCountryCG(coord)); + }); + Promise.all(promises).then((values) => { + let unique = new Set(values); + if (unique.size === 1) { + console.log(unique.values().next().value); + } else { + this.getCountryBDC(location).then((data) => resolve(data)); + } + }); + }); + }; + + /** + * Returns a country code (Only using BDC) + * Do not use externally - Used by getCountryCodeLocally + * Ultimately we will call our own API here and remove/ + * replace getCountryCode + * @param {Object} location {lat, lng} + * @return {Promise} Country code Promise + */ + static getCountryBDC = async (location) => { + return axios + .get(`https://api.bigdatacloud.net/data/reverse-geocode?latitude=${location.lat}&longitude=${location.lng}&key=${process.env.BDC_KEY}`) + .then((res) => countryCodes[res.data.countryCode]) + .catch((error) => error); + }; + + /** + * Returns a country code (Only using CodeGrid) + * Do not use externally - Used by getCountryCodeLocally + * @param {Object} location {lat, lng} + * @return {Promise} Country code Promise + */ + static getCountryCG = (location) => { + return new Promise((resolve, reject) => { + CG.getCode(location.lat, location.lng, (error, code) => { + if (error) { + reject(new Error(error)); + } else { + resolve(countryCodes[code.toUpperCase()]); + } + }); + }); + }; + + /** + * Returns an array of 9 coodinates as objects. + * Each coordinate is 100 meters aways from the given + * coordinate y angles from 0 to 315 + * The first coordinate is the original passed + * @param {Object} location {lat, lng} + * @return {Array} Coordinates [{lat, lng}, {lat, lng}] x 8 + */ + static getSurroundings = (location) => { + const meters = 100; + const R_EARTH = 6378.137; + const M = 1 / (((2 * Math.PI) / 360) * R_EARTH) / 1000; + + function moveFrom(coords, angle, distance) { + let radianAngle = (angle * Math.PI) / 180; + let x = 0 + distance * Math.cos(radianAngle); + let y = 0 + distance * Math.sin(radianAngle); + let newLat = coords.lat + y * M; + let newLng = coords.lng + (x * M) / Math.cos(coords.lat * (Math.PI / 180)); + return { lat: newLat, lng: newLng }; + } + let coordinates = [location]; + for (let angle = 0; angle < 360; angle += 45) { + coordinates.push(moveFrom({ lat: location.lat, lng: location.lng }, angle, meters)); + } + return coordinates; + }; + + /** + * Check if the param is coordinates + * @param {string} coordinates + * @return {boolean} + */ + static isCoordinates = (coordinates) => { + const regex = /^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$/g; + return regex.test(coordinates); + }; + + /** + * Returns map scale + * @param {Object} bounds map bounds + * @return {number} map scale + */ + static calculateScale = (bounds) => + GameHelper.haversineDistance({ lat: bounds.min.lat, lng: bounds.min.lng }, { lat: bounds.max.lat, lng: bounds.max.lng }) / 7.458421; + + /** + * Returns distance in km between two coordinates + * @param {Object} mk1 {lat, lng} + * @param {Object} mk2 {lat, lng} + * @return {number} km + */ + static haversineDistance = (mk1, mk2) => { + const R = 6371.071; + const rlat1 = mk1.lat * (Math.PI / 180); + const rlat2 = mk2.lat * (Math.PI / 180); + const difflat = rlat2 - rlat1; + const difflon = (mk2.lng - mk1.lng) * (Math.PI / 180); + const km = + 2 * + R * + Math.asin(Math.sqrt(Math.sin(difflat / 2) * Math.sin(difflat / 2) + Math.cos(rlat1) * Math.cos(rlat2) * Math.sin(difflon / 2) * Math.sin(difflon / 2))); + return km; + }; + + /** + * Returns score based on distance and scale + * @param {number} distance + * @param {number} scale + * @return {number} score + */ + static calculateScore = (distance, scale) => Math.round(5000 * Math.pow(0.99866017, (distance * 1000) / scale)); + + /** + * Returns guesses sorted by distance ASC + * @param {array} guesses + * @return {array} guesses + */ + static sortByDistance = (guesses) => guesses.sort((a, b) => a.distance - b.distance); + + /** + * Returns guesses sorted by score DESC + * @param {array} guesses + * @return {array} guesses + */ + static sortByScore = (guesses) => guesses.sort((a, b) => b.score - a.score); + + /** Converts a country code into an emoji flag + * @param {String} value + */ + static toEmojiFlag = (value) => { + if (value.length == 2) { + return value.toUpperCase().replace(/./g, (char) => String.fromCodePoint(char.charCodeAt(0) + 127397)); + } else { + const flag = value + .toUpperCase() + .substring(0, 2) + .replace(/./g, (char) => String.fromCodePoint(char.charCodeAt(0) + 127397)); + const region = value + .toUpperCase() + .substring(2) + .replace(/./g, (char) => String.fromCodePoint(char.charCodeAt(0) + 127397) + " "); + return `${flag} ${region}`.trim(); + } + }; + + /** Replace special chars + * @param {String} val + */ + static normalize = (val) => val.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); + + /** Matches words above 3 letters + * @param {String} input + * @param {String} key + */ + static isMatch = (input, key) => input.length >= 3 && key.includes(input) && input.length <= key.length; + + /** Find country by code or name + * @param {String} input + * @return {Object} countryCodesNames + */ + static findCountry = (input) => { + const normalized = GameHelper.normalize(input); + return countryCodesNames.find((country) => country.code === normalized || GameHelper.isMatch(normalized, country.names.toLowerCase())); + }; + + /** Return a random country code + * @return {String} + */ + static getRandomFlag = () => countryCodesNames[Math.floor(Math.random() * countryCodesNames.length)].code; + + /** Make game summary link + * @param {string} streamer + * @param {string} mapName + * @param {Object} mode + * @param {Object[]} locations + * @param {Object[]} scores + * @return {Promise} link + */ + static makeLink = (streamer, mapName, mode, locations, totalScores) => { + const players = totalScores.map((guess) => { + return { username: guess.username, flag: guess.flag, score: guess.score, rounds: guess.rounds }; + }); + + return axios + .post(`${process.env.API_URL}/game`, { + streamer: streamer, + map: mapName, + mode: mode, + locations: locations, + players: players, + }) + .then((res) => { + return `${process.env.BASE_URL}/game/${res.data.code}`; + }) + .catch((err) => { + console.log(err); + }); + }; +} + +module.exports = GameHelper; +const GameHelper = require("./GameHelper"); + + + +function mainWindow() { + let win = new BrowserWindow({ + show: false, + webPreferences: { + preload: path.join(__dirname, "../preload.js"), + enableRemoteModule: true, + contextIsolation: false, + webSecurity: false, + devTools: false, + }, + }); + win.setMenuBarVisibility(false); + win.loadURL("https://www.geoguessr.com/classic"); + + win.webContents.on("new-window", (e, link) => { + e.preventDefault(); + shell.openExternal(link); + }); + + win.on("closed", () => { + win = null; + }); + + return win; +} + +module.exports = mainWindow(); +const path = require("path"); +const { BrowserWindow } = require("electron"); + +function updateWindow() { + const win = new BrowserWindow({ + width: 400, + minWidth: 400, + height: 240, + minHeight: 240, + frame: false, + transparent: true, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + devTools: false, + }, + }); + win.setMenuBarVisibility(false); + win.loadURL(path.join(__dirname, "./update.html")); + + return win; +} + +module.exports = updateWindow(); + +const ipcRenderer = require("electron").ipcRenderer; + +const message = document.getElementById("message"); +const restartButton = document.getElementById("restart-button"); + +ipcRenderer.on("download_progress", () => { + ipcRenderer.removeAllListeners("download_progress"); + message.innerHTML = `Download in progress...`; +}); + +ipcRenderer.on("update_downloaded", () => { + ipcRenderer.removeAllListeners("update_downloaded"); + message.innerHTML = ` + Update downloaded successfully. It will be installed on restart.
+ Restart now ? + `; + restartButton.classList.remove("hidden"); +}); + +ipcRenderer.on("update_error", (err) => { + ipcRenderer.removeAllListeners("update_error"); + message.innerHTML = "An error occured."; +}); + +function closeWindow() { + ipcRenderer.send("close_update_window"); +} +function restartApp() { + ipcRenderer.send("restart_app"); +} +const path = require("path"); +const { BrowserWindow, shell } = require("electron"); + +function settingsWindow() { + const win = new BrowserWindow({ + width: 600, + minWidth: 600, + height: 500, + minHeight: 500, + show: false, + frame: false, + transparent: true, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + devTools: false, + }, + }); + win.setMenuBarVisibility(false); + win.loadURL(path.join(__dirname, "./settings.html")); + + win.webContents.on("new-window", (e, link) => { + e.preventDefault(); + shell.openExternal(link); + }); + + return win; +} + + + +const openTab = (e, tab) => { + const tabcontent = document.getElementsByClassName("tabcontent"); + for (let i = 0; i < tabcontent.length; i++) { + tabcontent[i].style.display = "none"; + } + const tablinks = document.getElementsByClassName("tablinks"); + for (let i = 0; i < tablinks.length; i++) { + tablinks[i].className = tablinks[i].className.replace(" active", ""); + } + document.getElementById(tab).style.display = "block"; + e.currentTarget.className += " active"; +}; +document.getElementById("defaultOpen").click(); +const GameHelper = require("../utils/GameHelper"); +const Store = require("../utils/Store"); +const Guess = require("./Guess"); + +class Game { + constructor() { + this.win; + this.settings; + this.url; + this.seed; + this.mapScale; + this.location; + this.country; + this.isInGame = false; + this.guessesOpen = false; + this.isMultiGuess = false; + this.guesses = []; + this.total = []; + this.lastLocation = {}; + } + + init = (win, settings) => { + this.win = win; + this.settings = settings; + this.lastLocation = Store.get("lastLocation", {}); + }; + + start = async (url, isMultiGuess) => { + this.isInGame = true; + this.isMultiGuess = isMultiGuess; + if (this.url === url) { + this.refreshSeed(); + } else { + this.url = url; + this.seed = await this.getSeed(); + this.mapScale = GameHelper.calculateScale(this.seed.bounds); + this.getCountry(); + this.clearGuesses(); + } + }; + + outGame = () => { + this.isInGame = false; + this.closeGuesses(); + }; + + streamerHasguessed = (newSeed) => newSeed.player.guesses.length != this.seed.player.guesses.length; + + locHasChanged = (newSeed) => JSON.stringify(newSeed.rounds[newSeed.rounds.length - 1]) != JSON.stringify(this.getLocation()); + + refreshSeed = async () => { + const newSeed = await this.getSeed(); + // If a guess has been comitted, process streamer guess then return scores + if (this.streamerHasguessed(newSeed)) { + this.win.webContents.send("pre-round-results"); + this.closeGuesses(); + this.seed = newSeed; + + const location = this.location; + const scores = await this.makeGuess().then(() => this.getRoundScores()); + return { location, scores }; + // Else, if only the loc has changed, the location was skipped, replace current loc + } else if (this.locHasChanged(newSeed)) { + this.seed = newSeed; + this.getCountry(); + return false; + } + }; + + getSeed = async () => await GameHelper.fetchSeed(this.url); + + getCountry = async () => { + this.location = this.getLocation(); + this.country = await GameHelper.getCountryCode(this.location); + }; + + makeGuess = async () => { + this.seed = await this.getSeed(); + + if (this.isMultiGuess) await this.processMultiGuesses(); + const streamerGuess = await this.processStreamerGuess(); + + this.guesses.push(streamerGuess); + this.guesses.forEach((guess) => this.pushToTotal(guess)); + + this.lastLocation = { lat: this.location.lat, lng: this.location.lng }; + Store.set("lastLocation", this.lastLocation); + + if (this.seed.state != "finished") { + this.getCountry(); + } + }; + + processMultiGuesses = () => { + let promises = []; + this.guesses.forEach(async (guess, index) => { + promises.push( + new Promise(async (resolve, reject) => { + const guessedCountry = await GameHelper.getCountryCode(guess.position); + guessedCountry === this.country ? guess.streak++ : (guess.streak = 0); + this.guesses[index].streak = guess.streak; + Store.setUserStreak(guess.user, guess.streak); + resolve(); + }) + ); + }); + return Promise.all(promises); + }; + + processStreamerGuess = async () => { + const index = this.seed.state === "finished" ? 1 : 2; + const streamerGuess = this.seed.player.guesses[this.seed.round - index]; + const location = { lat: streamerGuess.lat, lng: streamerGuess.lng }; + + const streamer = Store.getOrCreateUser(this.settings.channelName, this.settings.channelName); + const guessedCountry = await GameHelper.getCountryCode(location); + guessedCountry === this.country ? streamer.addStreak() : streamer.setStreak(0); + + const distance = GameHelper.haversineDistance(location, this.location); + const score = streamerGuess.timedOut ? 0 : GameHelper.calculateScore(distance, this.mapScale); + if (score == 5000) streamer.perfects++; + streamer.calcMeanScore(score); + + streamer.nbGuesses++; + Store.saveUser(this.settings.channelName, streamer); + + return new Guess(streamer.username, streamer.username, "#FFF", streamer.flag, location, streamer.streak, distance, score); + }; + + handleUserGuess = async (userstate, location) => { + const index = this.hasGuessedThisRound(userstate.username); + + if (!this.isMultiGuess && index != -1) return "alreadyGuessed"; + + const user = Store.getOrCreateUser(userstate.username, userstate["display-name"]); + if (this.hasPastedPreviousGuess(user.previousGuess, location)) return "pastedPreviousGuess"; + + if (JSON.stringify(user.lastLocation) != JSON.stringify(this.lastLocation)) user.setStreak(0); + + if (!this.isMultiGuess) { + const guessedCountry = await GameHelper.getCountryCode(location); + guessedCountry === this.country ? user.addStreak() : user.setStreak(0); + } + + const distance = GameHelper.haversineDistance(location, this.location); + const score = GameHelper.calculateScore(distance, this.mapScale); + if (score == 5000) user.perfects++; + user.calcMeanScore(score); + + const guess = new Guess(userstate.username, userstate["display-name"], userstate.color, user.flag, location, user.streak, distance, score); + + // Modify guess or push it + if (this.isMultiGuess && index != -1) { + this.guesses[index] = guess; + this.guesses[index].modified = true; + } else { + user.nbGuesses++; + this.guesses.push(guess); + } + + user.setLastLocation({ lat: this.location.lat, lng: this.location.lng }); + user.setPreviousGuess(location); + + Store.saveUser(userstate.username, user); + + return { user: user, guess: guess }; + }; + + nextRound = () => { + this.guesses = []; + if (this.seed.state != "finished") { + this.win.webContents.send("next-round", this.isMultiGuess); + } else { + this.win.webContents.send("final-results"); + } + }; + + getLocation = () => this.seed.rounds[this.seed.round - 1]; + + getLocations = () => { + return this.seed.rounds.map((round) => { + return { + lat: round.lat, + lng: round.lng, + heading: Math.round(round.heading), + pitch: Math.round(round.pitch), + }; + }); + }; + + openGuesses = () => { + this.guessesOpen = true; + }; + + closeGuesses = () => { + this.guessesOpen = false; + }; + + clearGuesses = () => { + this.guesses = []; + this.total = []; + }; + + /** + * @param {string} user + * @return {Number} index + */ + hasGuessedThisRound = (user) => this.guesses.findIndex((e) => e.user === user); + + /** + * @param {Object} previousGuess + * @param {Object} location {lat, lng} + * @return {boolean} + */ + hasPastedPreviousGuess = (previousGuess, location) => { + if (previousGuess === null) return false; + return previousGuess.lat === location.lat && previousGuess.lng === location.lng; + }; + + /** + * @param {Object} guess + */ + pushToTotal = (guess) => { + const index = this.total.findIndex((e) => e.user === guess.user); + if (index != -1) { + this.total[index].scores.push({ round: this.seed.round - 1, score: guess.score }); + this.total[index].score += guess.score; + this.total[index].distance += guess.distance; + this.total[index].streak = guess.streak; + this.total[index].color = guess.color; + this.total[index].flag = guess.flag; + this.total[index].rounds++; + } else { + this.total.push({ scores: [{ round: this.seed.round, score: guess.score }], ...guess, rounds: 1 }); + } + }; + + /** + * @return {Guess[]} sorted guesses by Distance + */ + getRoundScores = () => GameHelper.sortByDistance(this.guesses); + + /** + * @return {Guess[]} sorted guesses by Score + */ + getTotalScores() { + const scores = GameHelper.sortByScore(this.total); + // TODO: Remember to check equality + Store.userAddVictory(scores[0].user); + return scores; + } + + get mapName() { + return this.seed.mapName; + } + + get mode() { + return { noMove: this.seed.forbidMoving, noPan: this.seed.forbidRotating, noZoom: this.seed.forbidZooming }; + } + + get round() { + return this.seed.round; + } + + get nbGuesses() { + return this.guesses.length; + } +} + +module.exports = Game; +class Guess { + /** + * @param {String} user + * @param {String} username + * @param {String} color + * @param {String} flag + * @param {Object} position {lat, lng} + * @param {Number} streak + * @param {Number} distance + * @param {Number} score + * @param {Boolean} modified + */ + constructor(user, username, color, flag, position, streak, distance, score, modified = false) { + this.user = user; + this.username = username; + this.color = color === null ? "#FFF" : color; + this.flag = flag; + this.position = position; + this.streak = streak; + this.distance = distance; + this.score = score; + this.modified = modified; + } +} + +module.exports = Guess; +const Store = require("../../utils/Store"); + +class Scoreboard { + constructor() { + this.visibility; + this.position; + this.container; + this.scoreboard; + this.title; + this.switchContainer; + this.switchBtn; + this.table; + this.columnState; + this.isMultiGuess = false; + this.isResults = false; + this.isScrolling = false; + this.speed = 50; + this.init(); + } + + init() { + this.visibility = this.getCookie("visibility", true); + this.position = this.getCookie("scoreboard_position", { top: 20, left: 5, width: 380, height: 180 }); + this.container = $("#scoreboardContainer"); + this.title = $("#scoreboardTitle"); + this.switchContainer = $("#switchContainer"); + this.switchBtn = $("#switchBtn"); + + this.scoreboard = $("#scoreboard"); + this.scoreboard.css("top", this.position.top); + this.scoreboard.css("left", this.position.left); + this.scoreboard.css("width", this.position.width); + this.scoreboard.css("height", this.position.height); + this.scoreboard + .resizable({ + handles: "n, e, s, w, ne, se, sw, nw", + containment: "#scoreboardContainer", + }) + .draggable({ + containment: "#scoreboardContainer", + }) + .mouseup(() => { + const currentPosition = this.getPosition(); + if (JSON.stringify(this.position) !== JSON.stringify(currentPosition)) { + this.setPosition(currentPosition); + this.setCookie("scoreboard_position", JSON.stringify(currentPosition)); + } + }); + + this.switchBtn.on("change", () => { + if (this.switchBtn.is(":checked")) { + ipcRenderer.send("open-guesses"); + } else { + ipcRenderer.send("close-guesses"); + } + }); + + this.table = $("#datatable").DataTable({ + info: false, + searching: false, + paging: false, + scrollY: 100, + scrollResize: true, + scrollCollapse: true, + language: { zeroRecords: " " }, + dom: "Bfrtip", + buttons: [ + { + extend: "colvis", + text: "⚙️", + className: "colvis-btn", + columns: ":not(.noVis)", + }, + ], + columns: [ + { data: "Position" }, + { data: "Player" }, + { data: "Streak" }, + { + data: "Distance", + render: (data, type) => { + if (type === "display" || type === "filter") { + return this.toMeter(data); + } + return data; + }, + }, + { data: "Score" }, + ], + columnDefs: [ + { targets: 0, width: "35px", className: "noVis" }, + { targets: 1, width: "auto", className: "noVis" }, + { targets: 2, width: "55px" }, + { targets: 3, width: "100px" }, + { targets: 4, width: "75px", type: "natural" }, + ], + }); + + // Column Visisbility + this.columnState = this.getCookie("CG_ColVis", [ + { column: 0, state: true }, + { column: 2, state: true }, + { column: 3, state: true }, + { column: 4, state: true }, + ]); + + // Handle ColVis change + this.table.on("column-visibility.dt", (e, settings, column, state) => { + if (this.isResults || this.isMultiGuess) return; + + const i = this.columnState.findIndex((o) => o.column === column); + if (this.columnState[i]) { + this.columnState[i] = { column, state }; + } else { + this.columnState.push({ column, state }); + } + + this.setCookie("CG_ColVis", JSON.stringify(this.columnState)); + }); + + // SCROLLER + const sliderElem = ``; + $(".dt-buttons").append(sliderElem); + + const slider = document.getElementById("scrollSpeedSlider"); + + slider.oninput = (e) => { + this.speed = e.currentTarget.value; + this.scroller(".dataTables_scrollBody"); + }; + + const scrollBtn = ` +
+ +
+ `; + $(".dt-buttons").prepend(scrollBtn); + + $("#scrollBtn").on("change", (e) => { + if (e.currentTarget.checked != true) { + this.isScrolling = $(e.currentTarget).is(":checked"); + this.stop(".dataTables_scrollBody"); + slider.style.display = "none"; + } else { + this.isScrolling = $(e.currentTarget).is(":checked"); + this.scroller(".dataTables_scrollBody"); + slider.style.display = "inline"; + } + }); + } + + /** + * @param {boolean} isMultiGuess + */ + reset = (isMultiGuess) => { + this.isMultiGuess = isMultiGuess; + this.setColVis(); + this.isResults = false; + this.setTitle("GUESSES (0)"); + this.showSwitch(true); + this.table.clear().draw(); + }; + + setVisibility = () => { + this.visibility = !this.visibility; + this.setCookie("visibility", this.visibility); + this.checkVisibility(); + }; + + checkVisibility = () => { + if (this.visibility) { + this.show(); + } else { + this.hide(); + } + }; + + show = () => { + this.container.show(); + }; + + hide = () => { + this.container.hide(); + }; + + renderGuess = (guess) => { + const row = { + Position: "", + Player: `${guess.flag ? `` : ""}${ + guess.username + }`, + Streak: guess.streak, + Distance: guess.distance, + Score: guess.score, + }; + + const rowNode = this.table.row.add(row).node(); + rowNode.classList.add("expand"); + setTimeout(() => { + rowNode.classList.remove("expand"); + }, 200); + + this.table.order([3, "asc"]).draw(false); + this.table + .column(0) + .nodes() + .each((cell, i) => { + cell.innerHTML = i + 1; + }); + }; + + renderMultiGuess = (guesses) => { + const rows = guesses.map((guess) => { + return { + Position: "", + Player: `${guess.flag ? `` : ""}${ + guess.username + }`, + Streak: "", + Distance: "", + Score: "", + }; + }); + + this.table.clear().draw(); + this.table.rows.add(rows).draw(); + }; + + displayScores = (scores, isTotal = false) => { + this.isResults = true; + if (scores[0]) scores[0].color = "#E3BB39"; + if (scores[1]) scores[1].color = "#C9C9C9"; + if (scores[2]) scores[2].color = "#A3682E"; + const rows = scores.map((score) => { + return { + Position: "", + Player: `${score.flag ? `` : ""}${ + score.username + }`, + Streak: score.streak, + Distance: score.distance, + Score: `${score.score}${isTotal ? " [" + score.rounds + "]" : ""}`, + }; + }); + + this.table.clear().draw(); + this.table.rows.add(rows); + + this.table.order([4, "desc"]).draw(false); + + let content; + this.table + .column(0) + .nodes() + .each((cell, i) => { + content = i + 1; + if (isTotal) { + if (i == 0) content = "🏆"; + else if (i == 1) content = "🥈"; + else if (i == 2) content = "🥉"; + } + + cell.innerHTML = content; + }); + + // Restore columns visibility + this.table.columns().visible(true); + this.toTop(".dataTables_scrollBody"); + }; + + scroller = (elem) => { + const div = $(elem); + + const loop = () => { + if (!this.isScrolling) return; + div.stop().animate({ scrollTop: div[0].scrollHeight }, (div[0].scrollHeight - div.scrollTop() - 84) * this.speed, "linear", () => { + setTimeout(() => { + div.stop().animate({ scrollTop: 0 }, 1000, "swing", () => { + setTimeout(() => { + loop(); + }, 3000); + }); + }, 1000); + }); + }; + loop(); + }; + + toTop = (elem) => { + this.stop(elem); + setTimeout(() => { + this.scroller(elem); + }, 3000); + }; + + stop(elem) { + $(elem).stop(); + } + + setColVis = () => { + if (this.isMultiGuess) { + this.table.columns([0, 2, 3, 4]).visible(false); + } else { + this.columnState.forEach((column) => { + this.table.column(column.column).visible(column.state); + }); + } + }; + + getPosition = () => ({ + top: this.scoreboard.position().top, + left: this.scoreboard.position().left, + width: this.scoreboard.width(), + height: this.scoreboard.height(), + }); + + setPosition = (position) => (this.position = position); + + setTitle = (title) => this.title.text(title); + + showSwitch = (state) => this.switchContainer.css("display", state ? "block" : "none"); + + switchOn = (state) => this.switchBtn.prop("checked", state); + + toMeter = (distance) => (distance >= 1 ? parseFloat(distance.toFixed(1)) + "km" : parseInt(distance * 1000) + "m"); + + // ColVis Cookies + setCookie = (name, value, exdays = 60) => { + const d = new Date(); + d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000); + const expires = "expires=" + d.toUTCString(); + document.cookie = name + "=" + value + ";" + expires + ";path=/"; + }; + + getCookie = (name, defaultValue = {}) => { + const cname = name + "="; + var decodedCookie = decodeURIComponent(document.cookie); + var ca = decodedCookie.split(";"); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == " ") { + c = c.substring(1); + } + if (c.indexOf(cname) == 0) { + return JSON.parse(c.substring(cname.length, c.length)); + } + } + return defaultValue; + }; +} + +module.exports = Scoreboard; +class Settings { + /** + * @param {String} channelName="" + * @param {String} botUsername="" + * @param {String} token="" + * @param {String} cgCmd="!cg" + * @param {String} cgMsg="To play along, go to this link, pick a location, and paste the whole command into chat: " + * @param {String} userGetStatsCmd="!me" + * @param {String} userClearStatsCmd="!clear" + * @param {String} setStreakCmd="!setstreak" + * @param {Boolean} showHasGuessed=true + * @param {Boolean} isMultiGuess=false + * @param {Boolean} noCar=false + * @param {Boolean} noCompass=false + */ + constructor( + channelName = "", + botUsername = "", + token = "", + cgCmd = "!cg", + cgMsg = "To play along, go to this link, pick a location, and paste the whole command into chat: ", + userGetStatsCmd = "!me", + userClearStatsCmd = "!clear", + setStreakCmd = "!setstreak", + showHasGuessed = true, + isMultiGuess = false, + noCar = false, + noCompass = false + ) { + this.channelName = channelName; + this.botUsername = botUsername; + this.token = token; + this.cgCmd = cgCmd; + this.cgMsg = cgMsg; + this.userGetStatsCmd = userGetStatsCmd; + this.userClearStatsCmd = userClearStatsCmd; + this.setStreakCmd = setStreakCmd; + this.showHasGuessed = showHasGuessed; + this.isMultiGuess = isMultiGuess; + this.noCar = noCar; + this.noCompass = noCompass; + } + + /** + * @param {boolean} noCar + * @param {boolean} noCompass + */ + setGameSettings(isMultiGuess, noCar, noCompass) { + this.isMultiGuess = isMultiGuess; + this.noCar = noCar; + this.noCompass = noCompass; + } + + /** + * @param {Object} commands + */ + setTwitchCommands(commands) { + this.cgCmd = commands.cgCmdd; + this.cgMsg = commands.cgMsgg; + this.userGetStatsCmd = commands.userGetStats; + this.userClearStatsCmd = commands.userClearStats; + this.setStreakCmd = commands.setStreak; + this.showHasGuessed = commands.showHasGuessed; + } + + /** + * @param {string} channelName + * @param {string} botUsername + * @param {string} token + */ + setTwitchSettings(channelName, botUsername, token) { + this.channelName = channelName; + this.botUsername = botUsername; + this.token = token; + } +} + +module.exports = Settings; +class User { + /** + * @param {String} user + * @param {String} username + * @param {String} flag="" + * @param {Number} streak=0 + * @param {Number} bestStreak=0 + * @param {Number} correctGuesses=0 + * @param {Number} nbGuesses=0 + * @param {Number} perfects=0 + * @param {Number} victories=0 + * @param {Number} meanScore=null + * @param {Object} previousGuess={} + * @param {Object} lastLocation=null + */ + constructor( + user, + username, + flag = "", + streak = 0, + bestStreak = 0, + correctGuesses = 0, + nbGuesses = 0, + perfects = 0, + victories = 0, + meanScore = null, + previousGuess = {}, + lastLocation = null + ) { + this.user = user; + this.username = username; + this.flag = flag; + this.streak = streak; + this.bestStreak = bestStreak; + this.correctGuesses = correctGuesses; + this.nbGuesses = nbGuesses; + this.perfects = perfects; + this.victories = victories; + this.meanScore = meanScore; + this.previousGuess = previousGuess; + this.lastLocation = lastLocation; + } + + /* Add 1 to streak and correctGuesses. */ + addStreak() { + this.streak++; + this.correctGuesses++; + if (this.streak > this.bestStreak) this.bestStreak = this.streak; + } + + /** Set user streak + * @param {Number} number + */ + setStreak(number) { + this.streak = number; + if (this.streak > this.bestStreak) this.bestStreak = this.streak; + } + + /** Set last location + * @param {Object} location + */ + setLastLocation(location) { + this.lastLocation = location; + } + + /** Set previous guess + * @param {Object} location + */ + setPreviousGuess(location) { + this.previousGuess = location; + } + + /** Set a country flag + * @param {String} flag country code + */ + setFlag(flag) { + this.flag = flag; + } + + /** Calculate mean score + * @param {Number} score + */ + calcMeanScore(score) { + if (this.meanScore === null) { + this.meanScore = score; + } else { + this.meanScore = (this.meanScore * this.nbGuesses + score) / (this.nbGuesses + 1); + } + } +} + diff --git a/web/chatguessr/index.html b/web/chatguessr/index.html new file mode 100644 index 0000000..a8cf929 --- /dev/null +++ b/web/chatguessr/index.html @@ -0,0 +1,51 @@ + + + + + + GeoGuess + + + + + + + + + +
+
+
+
+ +
+
+ + + + + + + +
#UserDistanceScore
1sora12301230
2Sorb12301230
3Sorc12301230
+
+
+ +
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/chatguessr/index.js b/web/chatguessr/index.js new file mode 100644 index 0000000..3caa3e6 --- /dev/null +++ b/web/chatguessr/index.js @@ -0,0 +1,126 @@ +$(function () { + const webSocket = window.socket; + var guesses = {}, + status = '-', + hidden = false; + + const getQueryMap = () => { + let queryString = window.location.search, + queryParts = queryString.substring(1).split('&'), + queryMap = new Map(); + + for (let part of queryParts) { + let key = part.substring(0, part.indexOf('=')), + value = part.substring(part.indexOf('=') + 1, part.length); + if (key.length > 0 && value.length > 0) queryMap.set(key, value); + } + return queryMap; + } + Map.prototype.getOrElse = (option, def) => queryMap.has(option) ? queryMap.get(option): def; + const queryMap = getQueryMap(); + + + + $('#showhide').on('click',()=>{ + if(hidden){ + $('.containing').show(); + hidden = false; + }else{ + $('.containing').hide(); + hidden = true; + } + }) + + const computeScore = (pos) => { + return 0; + } + const fillTable = ()=> { + $('#datatab > tr').remove(); + var html = ''; + for (let user in Object.keys(guesses)) + html += '' + user + '' + computeScore(guesses[user]) + ''; + $('#datatab').html(html); + } + + const refreshUI = () => { + // if(queryMap.getOrElse('single',false)){ + // $('#all').show(); $('#timer').hide(); $('#goal').hide(); + // $('#follow').parent().closest('div').hide(); $('#subscribe').parent().closest('div').hide(); $('#donate').parent().closest('div').hide(); + // if(queryMap.getOrElse('follow',false)) $('#follow').parent().closest('div').show(); + // if(queryMap.getOrElse('subscribe',false)) $('#subscribe').parent().closest('div').show(); + // if(queryMap.getOrElse('donate',false)) $('#donate').parent().closest('div').show(); + // }else if(queryMap.getOrElse('goal',false)){ + // $('#all').hide(); $('#timer').hide(); $('#goal').show(); + // $('#cfollow').parent().closest('div').hide(); $('#csubscribe').parent().closest('div').hide(); + // if(queryMap.getOrElse('follow',false)) $('#cfollow').parent().closest('div').show(); + // if(queryMap.getOrElse('subscribe',false)) $('#csubscribe').parent().closest('div').show(); + // } + + // $('#follow').text(lastFollow); + // $('#subscribe').text(lastSub); + // $('#donate').text(lastDonator); + // $('#cfollow').parent().closest('div').progressbar({ value : countFollow/parseInt(queryMap.getOrElse('follow',9999))*100 }); + // $('#csubscribe').parent().closest('div').progressbar({ value : countSub/parseInt(queryMap.getOrElse('subscribe',9999))*100}); + // $('#cfollow').text(`${countFollow} / ${queryMap.getOrElse('follow',9999)}`); + // $('#csubscribe').text(`${countSub} / ${queryMap.getOrElse('subscribe',9999)}`); + } + + const handleSocketMessage = (e)=>{ + try { + let rawMessage = e.data, + message = JSON.parse(rawMessage); + + if(!message.hasOwnProperty('eventFamily') || message.eventFamily != 'chatguessr' || + !message.hasOwnProperty('eventType') || !message.hasOwnProperty('data')) + return; + + console.log(message.eventType, message.data) + if(message.eventType == 'follow') { + lastFollow = message.data || lastFollow; + countFollow++; + } else if(message.eventType == 'subscribe') { + lastSub = message.data || lastSub; + countSub++; + } else if(message.eventType == 'donation') { + lastDonator = message.data || lastDonator; + } else if(message.eventType == 'timer') { + console.log("New timer !! ", message.data) + } + refreshUI(); + } catch (ex) { + console.log(ex) + } + }; + + + jQuery(async ()=>{ + refreshUI(); + try{ + socket.addFamilyHandler("chatguessr", handleSocketMessage); + if(socket){ + while(socket.getReadyState() === 0){ + await new Promise(r => setTimeout(r, 500)); + } + // socket.getDBValue("get_current_stream_info_lastFollow", 'streamInfo', 'lastFollow', (response)=>{ + // if(response.streamInfo) lastFollow = response.streamInfo || lastFollow; + // refreshUI(); + // }) + // socket.getDBValue("get_current_stream_info_lastSub", 'streamInfo', 'lastSub', (response)=>{ + // if(response.streamInfo) lastSub = response.streamInfo || lastSub; + // refreshUI(); + // }) + // socket.getDBValue("get_current_stream_info_lastDonator", 'streamInfo', 'lastDonator', (response)=>{ + // if(response.streamInfo) lastDonator = response.streamInfo || lastDonator; + // refreshUI(); + // }) + // socket.getDBValue('get_current_stream_info_counts', 'panelData', 'stream', (e)=>{ + // let data = JSON.parse(e.panelData); + // countFollow = data.followers || countFollow; + // countSub = 0; + // refreshUI(); + // }) + } + }catch(e) {console.log(e)} + }) + +}); \ No newline at end of file diff --git a/web/chatguessr/styles.css b/web/chatguessr/styles.css new file mode 100644 index 0000000..7535c4e --- /dev/null +++ b/web/chatguessr/styles.css @@ -0,0 +1,790 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; + --nord0: #2e3440; + --nord1: #3b4252; + --nord2: #434c5e; + --nord3: #4c566a; + --nord4: #d8dee9; + --nord5: #e5e9f0; + --nord6: #eceff4; + --nord7: #8fbcbb; + --nord8: #88c0d0; + --nord9: #81a1c1; + --blue: #5e81ac; + --red: #bf616a; + --orange: #d08770; + --yellow: #ebcb8b; + --green: #a3be8c; + --purple: #b48ead; + + --fonts: 'IBMPlex Mono', sans-serif; + } + + html, + body { + padding: 0; + margin: 0; + font-family: var(--fonts); + /* background-color: var(--nord0); */ + color: var(--nord4); + } + + a, + a:visited { + color: var(--nord7); + text-decoration: none !important; + } + a:hover { + color: var(--blue) !important; + } + + .red a:hover, + .red { + color: var(--red); + } + + .purple a:hover, + .purple { + color: var(--purple); + } + + .green a:hover, + .green { + color: var(--green); + } + + .yellow a:hover, + .yellow { + color: var(--yellow); + } + + .blue a:hover, + .blue { + color: var(--blue); + } + + .pointer { + cursor: pointer; + } + + .ui-progressbar-value { + height:100%; + position:absolute; + background-color: var(--blue); + border-radius: 0.25rem; + z-index: 0; + left:0; + } + + .no-border { + border:none!important; + } + + img { + object-fit:cover; + background-color: var(--nord3); + box-shadow: 8px 8px rgba(0, 0, 0, 0.2); + } + img.no-bg { + background-color: transparent; + box-shadow: none; + } + .fit { + width:fit-content; + height:fit-content; + } + + .img-wrap { + display: inline-block; width: 100%; height: 100%; + width:fit-content; + height:fit-content; + overflow:hidden; + } + .img-wrap > div:first-child, .img-wrap > span:first-child { + position: static !important + } + .img-wrap > div > img, .img-wrap > span > img { + position: inherit !important; + width:auto !important; + height:auto !important; + margin:0!important; + } + + form { + margin: 1.5em auto 1em auto; + display:block; + width:400px; + + } + input { + width: 100%; + padding: 0.5em; + border:none; + border-radius:0; + background-color: var(--nord1); + color: var(--nord4); + box-shadow: 8px 8px rgba(0, 0, 0, 0.2); + } + + .card { + scrollbar-color: var(--nord4) var(--nord1); + scrollbar-width: thin; + box-shadow: 8px 8px rgba(0, 0, 0, 0.2); + background-color: var(--nord1); + } + + header { + padding-bottom: 8px; + font-weight: bold; + cursor: pointer; + } + sub{ + font-weight:normal; + font-size:0.7em; + cursor: pointer; + } + + article>hr{ + margin-top: 1em; + margin-bottom: 1em; + } + + .tshadow{ + text-shadow: 0 0 0.25em var(--nord0); + } + + .opacity-4{opacity:0.4;} + .opacity-5{opacity:0.5;} + .grayscale-5{filter: grayscale(50%);} + .grayscale{filter: grayscale(100%);} + .index-100{z-index:100;} + + /* purgecss end ignore */ + + .h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2} + .h1,h1{font-size:calc(1.375rem + 1.5vw)} + .h2,h2{font-size:calc(1.325rem + .9vw)} + .h3,h3{font-size:calc(1.3rem + .6vw)} + .h4,h4{font-size:calc(1.275rem + .3vw)} + .h5,h5{font-size:1.25rem} + .h6,h6{font-size:1rem} + p{margin-top:0;margin-bottom:1rem} + abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none} + address{margin-bottom:1rem;font-style:normal;line-height:inherit} + ol,ul{padding-left:2rem} + dl,ol,ul{margin-top:0;margin-bottom:1rem} + ol ol,ol ul,ul ol,ul ul{margin-bottom:0} + dt{font-weight:700} + dd{margin-bottom:.5rem;margin-left:0} + blockquote{margin:0 0 1rem} + b,strong{font-weight:bolder} + + + .small,small{font-size:.875em} + .mark,mark{padding:.2em;background-color:--red} + sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline} + sub{bottom:-.25em} + sup{top:-.5em} + + a:not([href]):not([class]), + a:not([href]):not([class]):hover{color:inherit;text-decoration:none} + code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override} + pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em} + pre code{font-size:inherit;color:inherit;word-break:normal} + code{font-size:.875em;color:var(--nord9);word-wrap:break-word} + a>code{color:inherit} + kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem} + kbd kbd{padding:0;font-size:1em;font-weight:700} + figure{margin:0 0 1rem} + img,svg{vertical-align:middle} + table{caption-side:bottom;border-collapse:collapse} + caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--nord3);text-align:left} + th{text-align:inherit;text-align:-webkit-match-parent} + tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0} + label{display:inline-block} + button{border-radius:0} + button:focus:not(:focus-visible){outline:0} + button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit} + button,select{text-transform:none}[role=button]{cursor:pointer} + select{word-wrap:normal} + select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none} + textarea{resize:vertical} + fieldset{min-width:0;padding:0;margin:0;border:0} + legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit} + legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button} + output{display:inline-block} + iframe{border:0} + summary{display:list-item;cursor:pointer} + progress{vertical-align:baseline}[hidden]{display:none!important} + .lead{font-size:1.25rem;font-weight:300} + .display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2} + .display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2} + .display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2} + .display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2} + .display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2} + .display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2} + .list-unstyled{padding-left:0;list-style:none} + .list-inline{padding-left:0;list-style:none} + .list-inline-item{display:inline-block} + .list-inline-item:not(:last-child){margin-right:.5rem} + .initialism{font-size:.875em;text-transform:uppercase} + .blockquote{margin-bottom:1rem;font-size:1.25rem} + .blockquote>:last-child{margin-bottom:0} + .blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d} + .blockquote-footer::before{content:"— "} + .img-fluid{max-width:100%;height:auto} + .img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto} + .figure{display:inline-block} + .figure-img{margin-bottom:.5rem;line-height:1} + .figure-caption{font-size:.875em;color:#6c757d} + .container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto} + .row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x) * -.5);margin-left:calc(var(--bs-gutter-x) * -.5)} + .row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)} + .col{flex:1 0 0%} + .row-cols-auto>*{flex:0 0 auto;width:auto} + .row-cols-1>*{flex:0 0 auto;width:100%} + .row-cols-2>*{flex:0 0 auto;width:50%} + .row-cols-3>*{flex:0 0 auto;width:33.3333333333%} + .row-cols-4>*{flex:0 0 auto;width:25%} + .row-cols-5>*{flex:0 0 auto;width:20%} + .row-cols-6>*{flex:0 0 auto;width:16.6666666667%} + .col-auto{flex:0 0 auto;width:auto} + .col-1{flex:0 0 auto;width:8.33333333%} + .col-2{flex:0 0 auto;width:16.66666667%} + .col-3{flex:0 0 auto;width:25%} + .col-4{flex:0 0 auto;width:33.33333333%} + .col-5{flex:0 0 auto;width:41.66666667%} + .col-6{flex:0 0 auto;width:50%} + .col-7{flex:0 0 auto;width:58.33333333%} + .col-8{flex:0 0 auto;width:66.66666667%} + .col-9{flex:0 0 auto;width:75%} + .col-10{flex:0 0 auto;width:83.33333333%} + .col-11{flex:0 0 auto;width:91.66666667%} + .col-12{flex:0 0 auto;width:100%} + .offset-1{margin-left:8.33333333%} + .offset-2{margin-left:16.66666667%} + .offset-3{margin-left:25%} + .offset-4{margin-left:33.33333333%} + .offset-5{margin-left:41.66666667%} + .offset-6{margin-left:50%} + .offset-7{margin-left:58.33333333%} + .offset-8{margin-left:66.66666667%} + .offset-9{margin-left:75%} + .offset-10{margin-left:83.33333333%} + .offset-11{margin-left:91.66666667%} + .g-0,.gx-0{--bs-gutter-x:0} + .g-0,.gy-0{--bs-gutter-y:0} + .g-1,.gx-1{--bs-gutter-x:0.25rem} + .g-1,.gy-1{--bs-gutter-y:0.25rem} + .g-2,.gx-2{--bs-gutter-x:0.5rem} + .g-2,.gy-2{--bs-gutter-y:0.5rem} + .g-3,.gx-3{--bs-gutter-x:1rem} + .g-3,.gy-3{--bs-gutter-y:1rem} + .g-4,.gx-4{--bs-gutter-x:1.5rem} + .g-4,.gy-4{--bs-gutter-y:1.5rem} + .g-5,.gx-5{--bs-gutter-x:3rem} + .g-5,.gy-5{--bs-gutter-y:3rem} + .card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem} + .card>hr{margin-right:0;margin-left:0} + .card>.list-group{border-top:inherit;border-bottom:inherit} + .card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)} + .card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)} + .card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0} + .card-body{flex:1 1 auto;padding:1rem 1rem} + .card-title{margin-bottom:.5rem} + .card-subtitle{margin-top:-.25rem;margin-bottom:0} + .card-text:last-child{margin-bottom:0} + .card-link:hover{text-decoration:none} + .card-link+.card-link{margin-left:1rem} + .card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)} + .card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0} + .card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)} + .card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)} + .card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0} + .card-header-pills{margin-right:-.5rem;margin-left:-.5rem} + .card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)} + .card-img,.card-img-bottom,.card-img-top{width:100%} + .card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)} + .card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)} + .card-group>.card{margin-bottom:.75rem} + .card-group>.card{flex:1 0 0%;margin-bottom:0} + .card-group>.card+.card{margin-left:0;border-left:0} + .card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0} + .card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0} + .card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0} + .card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0} + .card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0} + .card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}} + .spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border} + .spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}} + .spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow} + .spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}} + .offcanvas{position:fixed;bottom:0;z-index:1050;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out} + @media (prefers-reduced-motion:reduce){.offcanvas{transition:none}} + .offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem} + .offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem} + .offcanvas-title{margin-bottom:0;line-height:1.5} + .offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto} + .offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)} + .offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)} + .offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)} + .offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)} + .offcanvas.show{transform:none} + .clearfix::after{display:block;clear:both;content:""} + .link-primary{color:#0d6efd} + .link-primary:focus,.link-primary:hover{color:#0a58ca} + .link-secondary{color:#6c757d} + .link-secondary:focus,.link-secondary:hover{color:#565e64} + .link-success{color:#198754} + .link-success:focus,.link-success:hover{color:#146c43} + .link-info{color:#0dcaf0} + .link-info:focus,.link-info:hover{color:#3dd5f3} + .link-warning{color:#ffc107} + .link-warning:focus,.link-warning:hover{color:#ffcd39} + .link-danger{color:#dc3545} + .link-danger:focus,.link-danger:hover{color:#b02a37} + .link-light{color:#f8f9fa} + .link-light:focus,.link-light:hover{color:#f9fafb} + .link-dark{color:#212529} + .link-dark:focus,.link-dark:hover{color:#1a1e21} + .ratio{position:relative;width:100%} + .ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""} + .ratio>*{position:absolute;top:0;left:0;width:100%;height:100%} + .ratio-1x1{--bs-aspect-ratio:100%} + .ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)} + .ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)} + .ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)} + .fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030} + .fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030} + .sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020} + .visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important} + .stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""} + .text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + .align-baseline{vertical-align:baseline!important} + .align-top{vertical-align:top!important} + .align-middle{vertical-align:middle!important} + .align-bottom{vertical-align:bottom!important} + .align-text-bottom{vertical-align:text-bottom!important} + .align-text-top{vertical-align:text-top!important} + .float-start{float:left!important} + .float-end{float:right!important} + .float-none{float:none!important} + .overflow-auto{overflow:auto!important} + .overflow-hidden{overflow:hidden!important} + .overflow-visible{overflow:visible!important} + .overflow-scroll{overflow:scroll!important} + .d-inline{display:inline!important} + .d-inline-block{display:inline-block!important} + .d-block{display:block!important} + .d-grid{display:grid!important} + .d-table{display:table!important} + .d-table-row{display:table-row!important} + .d-table-cell{display:table-cell!important} + .d-flex{display:flex!important} + .d-inline-flex{display:inline-flex!important} + .d-none{display:none!important} + .shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important} + .shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important} + .shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important} + .shadow-none{box-shadow:none!important} + .position-static{position:static!important} + .position-relative{position:relative!important} + .position-absolute{position:absolute!important} + .position-fixed{position:fixed!important} + .position-sticky{position:-webkit-sticky!important;position:sticky!important} + .top-0{top:0!important} + .top-50{top:50%!important} + .top-100{top:100%!important} + .bottom-0{bottom:0!important} + .bottom-50{bottom:50%!important} + .bottom-100{bottom:100%!important} + .start-0{left:0!important} + .start-50{left:50%!important} + .start-100{left:100%!important} + .end-0{right:0!important} + .end-50{right:50%!important} + .end-100{right:100%!important} + .translate-middle{transform:translate(-50%,-50%)!important} + .translate-middle-x{transform:translateX(-50%)!important} + .translate-middle-y{transform:translateY(-50%)!important} + .border{border:1px solid #dee2e6!important} + .border-0{border:0!important} + .border-top{border-top:1px solid #dee2e6!important} + .border-top-0{border-top:0!important} + .border-end{border-right:1px solid #dee2e6!important} + .border-end-0{border-right:0!important} + .border-bottom{border-bottom:1px solid #dee2e6!important} + .border-bottom-0{border-bottom:0!important} + .border-start{border-left:1px solid #dee2e6!important} + .border-start-0{border-left:0!important} + .border-primary{border-color:#0d6efd!important} + .border-secondary{border-color:#6c757d!important} + .border-success{border-color:#198754!important} + .border-info{border-color:#0dcaf0!important} + .border-warning{border-color:#ffc107!important} + .border-danger{border-color:#dc3545!important} + .border-light{border-color:#f8f9fa!important} + .border-dark{border-color:#212529!important} + .border-white{border-color:#fff!important} + .border-1{border-width:1px!important} + .border-2{border-width:2px!important} + .border-3{border-width:3px!important} + .border-4{border-width:4px!important} + .border-5{border-width:5px!important} + .w-25{width:25%!important} + .w-50{width:50%!important} + .w-75{width:75%!important} + .w-100{width:100%!important} + .w-auto{width:auto!important} + .mw-100{max-width:100%!important} + .vw-100{width:100vw!important} + .min-vw-100{min-width:100vw!important} + .h-25{height:25%!important} + .h-50{height:50%!important} + .h-75{height:75%!important} + .h-100{height:100%!important} + .h-auto{height:auto!important} + .mh-100{max-height:100%!important} + .vh-100{height:100vh!important} + .min-vh-100{min-height:100vh!important} + .flex-fill{flex:1 1 auto!important} + .flex-row{flex-direction:row!important} + .flex-column{flex-direction:column!important} + .flex-row-reverse{flex-direction:row-reverse!important} + .flex-column-reverse{flex-direction:column-reverse!important} + .flex-grow-0{flex-grow:0!important} + .flex-grow-1{flex-grow:1!important} + .flex-shrink-0{flex-shrink:0!important} + .flex-shrink-1{flex-shrink:1!important} + .flex-wrap{flex-wrap:wrap!important} + .flex-nowrap{flex-wrap:nowrap!important} + .flex-wrap-reverse{flex-wrap:wrap-reverse!important} + .gap-0{gap:0!important} + .gap-1{gap:.25rem!important} + .gap-2{gap:.5rem!important} + .gap-3{gap:1rem!important} + .gap-4{gap:1.5rem!important} + .gap-5{gap:3rem!important} + .justify-content-start{justify-content:flex-start!important} + .justify-content-end{justify-content:flex-end!important} + .justify-content-center{justify-content:center!important} + .justify-content-between{justify-content:space-between!important} + .justify-content-around{justify-content:space-around!important} + .justify-content-evenly{justify-content:space-evenly!important} + .align-items-start{align-items:flex-start!important} + .align-items-end{align-items:flex-end!important} + .align-items-center{align-items:center!important} + .align-items-baseline{align-items:baseline!important} + .align-items-stretch{align-items:stretch!important} + .align-content-start{align-content:flex-start!important} + .align-content-end{align-content:flex-end!important} + .align-content-center{align-content:center!important} + .align-content-between{align-content:space-between!important} + .align-content-around{align-content:space-around!important} + .align-content-stretch{align-content:stretch!important} + .align-self-auto{align-self:auto!important} + .align-self-start{align-self:flex-start!important} + .align-self-end{align-self:flex-end!important} + .align-self-center{align-self:center!important} + .align-self-baseline{align-self:baseline!important} + .align-self-stretch{align-self:stretch!important} + .order-first{order:-1!important} + .order-0{order:0!important} + .order-1{order:1!important} + .order-2{order:2!important} + .order-3{order:3!important} + .order-4{order:4!important} + .order-5{order:5!important} + .order-last{order:6!important} + .m-0{margin:0!important} + .m-1{margin:.25rem!important} + .m-2{margin:.5rem!important} + .m-3{margin:1rem!important} + .m-4{margin:1.5rem!important} + .m-5{margin:3rem!important} + .m-auto{margin:auto!important} + .mx-0{margin-right:0!important;margin-left:0!important} + .mx-1{margin-right:.25rem!important;margin-left:.25rem!important} + .mx-2{margin-right:.5rem!important;margin-left:.5rem!important} + .mx-3{margin-right:1rem!important;margin-left:1rem!important} + .mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important} + .mx-5{margin-right:3rem!important;margin-left:3rem!important} + .mx-auto{margin-right:auto!important;margin-left:auto!important} + .my-0{margin-top:0!important;margin-bottom:0!important} + .my-1{margin-top:.25rem!important;margin-bottom:.25rem!important} + .my-2{margin-top:.5rem!important;margin-bottom:.5rem!important} + .my-3{margin-top:1rem!important;margin-bottom:1rem!important} + .my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important} + .my-5{margin-top:3rem!important;margin-bottom:3rem!important} + .my-auto{margin-top:auto!important;margin-bottom:auto!important} + .mt-0{margin-top:0!important} + .mt-1{margin-top:.25rem!important} + .mt-2{margin-top:.5rem!important} + .mt-3{margin-top:1rem!important} + .mt-4{margin-top:1.5rem!important} + .mt-5{margin-top:3rem!important} + .mt-auto{margin-top:auto!important} + .me-0{margin-right:0!important} + .me-1{margin-right:.25rem!important} + .me-2{margin-right:.5rem!important} + .me-3{margin-right:1rem!important} + .me-4{margin-right:1.5rem!important} + .me-5{margin-right:3rem!important} + .me-auto{margin-right:auto!important} + .mb-0{margin-bottom:0!important} + .mb-1{margin-bottom:.25rem!important} + .mb-2{margin-bottom:.5rem!important} + .mb-3{margin-bottom:1rem!important} + .mb-4{margin-bottom:1.5rem!important} + .mb-5{margin-bottom:3rem!important} + .mb-auto{margin-bottom:auto!important} + .ms-0{margin-left:0!important} + .ms-1{margin-left:.25rem!important} + .ms-2{margin-left:.5rem!important} + .ms-3{margin-left:1rem!important} + .ms-4{margin-left:1.5rem!important} + .ms-5{margin-left:3rem!important} + .ms-auto{margin-left:auto!important} + .p-0{padding:0!important} + .p-1{padding:.25rem!important} + .p-2{padding:.5rem!important} + .p-3{padding:1rem!important} + .p-4{padding:1.5rem!important} + .p-5{padding:3rem!important} + .px-0{padding-right:0!important;padding-left:0!important} + .px-1{padding-right:.25rem!important;padding-left:.25rem!important} + .px-2{padding-right:.5rem!important;padding-left:.5rem!important} + .px-3{padding-right:1rem!important;padding-left:1rem!important} + .px-4{padding-right:1.5rem!important;padding-left:1.5rem!important} + .px-5{padding-right:3rem!important;padding-left:3rem!important} + .py-0{padding-top:0!important;padding-bottom:0!important} + .py-1{padding-top:.25rem!important;padding-bottom:.25rem!important} + .py-2{padding-top:.5rem!important;padding-bottom:.5rem!important} + .py-3{padding-top:1rem!important;padding-bottom:1rem!important} + .py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important} + .py-5{padding-top:3rem!important;padding-bottom:3rem!important} + .pt-0{padding-top:0!important} + .pt-1{padding-top:.25rem!important} + .pt-2{padding-top:.5rem!important} + .pt-3{padding-top:1rem!important} + .pt-4{padding-top:1.5rem!important} + .pt-5{padding-top:3rem!important} + .pe-0{padding-right:0!important} + .pe-1{padding-right:.25rem!important} + .pe-2{padding-right:.5rem!important} + .pe-3{padding-right:1rem!important} + .pe-4{padding-right:1.5rem!important} + .pe-5{padding-right:3rem!important} + .pb-0{padding-bottom:0!important} + .pb-1{padding-bottom:.25rem!important} + .pb-2{padding-bottom:.5rem!important} + .pb-3{padding-bottom:1rem!important} + .pb-4{padding-bottom:1.5rem!important} + .pb-5{padding-bottom:3rem!important} + .ps-0{padding-left:0!important} + .ps-1{padding-left:.25rem!important} + .ps-2{padding-left:.5rem!important} + .ps-3{padding-left:1rem!important} + .ps-4{padding-left:1.5rem!important} + .ps-5{padding-left:3rem!important} + .font-monospace{font-family:var(--bs-font-monospace)!important} + .fs-1{font-size:calc(1.375rem + 1.5vw)!important} + .fs-2{font-size:calc(1.325rem + .9vw)!important} + .fs-3{font-size:calc(1.3rem + .6vw)!important} + .fs-4{font-size:calc(1.275rem + .3vw)!important} + .fs-5{font-size:1.25rem!important} + .fs-6{font-size:1rem!important} + .fst-italic{font-style:italic!important} + .fst-normal{font-style:normal!important} + .fw-light{font-weight:300!important} + .fw-lighter{font-weight:lighter!important} + .fw-normal{font-weight:400!important} + .fw-bold{font-weight:700!important} + .fw-bolder{font-weight:bolder!important} + .lh-1{line-height:1!important} + .lh-sm{line-height:1.25!important} + .lh-base{line-height:1.5!important} + .lh-lg{line-height:2!important} + .text-start{text-align:left!important} + .text-end{text-align:right!important} + .text-center{text-align:center!important} + .text-decoration-none{text-decoration:none!important} + .text-decoration-underline{text-decoration:underline!important} + .text-decoration-line-through{text-decoration:line-through!important} + .text-lowercase{text-transform:lowercase!important} + .text-uppercase{text-transform:uppercase!important} + .text-capitalize{text-transform:capitalize!important} + .text-wrap{white-space:normal!important} + .text-nowrap{white-space:nowrap!important} + .text-break{word-wrap:break-word!important;word-break:break-word!important} + .text-primary{color:#0d6efd!important} + .text-secondary{color:#6c757d!important} + .text-success{color:#198754!important} + .text-info{color:#0dcaf0!important} + .text-warning{color:#ffc107!important} + .text-danger{color:#dc3545!important} + .text-light{color:#f8f9fa!important} + .text-dark{color:#212529!important} + .text-white{color:#fff!important} + .text-body{color:#212529!important} + .text-muted{color:#6c757d!important} + .text-black-50{color:rgba(0,0,0,.5)!important} + .text-white-50{color:rgba(255,255,255,.5)!important} + .text-reset{color:inherit!important} + .bg-primary{background-color:#0d6efd!important} + .bg-secondary{background-color:#6c757d!important} + .bg-success{background-color:#198754!important} + .bg-info{background-color:#0dcaf0!important} + .bg-warning{background-color:#ffc107!important} + .bg-danger{background-color:#dc3545!important} + .bg-light{background-color:#f8f9fa!important} + .bg-dark{background-color:#212529!important} + .bg-body{background-color:#fff!important} + .bg-white{background-color:#fff!important} + .bg-transparent{background-color:transparent!important} + .bg-gradient{background-image:var(--bs-gradient)!important} + .user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important} + .user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important} + .user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important} + .pe-none{pointer-events:none!important} + .pe-auto{pointer-events:auto!important} + .rounded{border-radius:.25rem!important} + .rounded-0{border-radius:0!important} + .rounded-1{border-radius:.2rem!important} + .rounded-2{border-radius:.25rem!important} + .rounded-3{border-radius:.3rem!important} + .rounded-circle{border-radius:50%!important} + .rounded-pill{border-radius:50rem!important} + .rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important} + .rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important} + .rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important} + .rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important} + .visible{visibility:visible!important} + .invisible{visibility:hidden!important} + + .fs-1{font-size:2.5rem!important} + .fs-2{font-size:2rem!important} + .fs-3{font-size:1.75rem!important} + .fs-4{font-size:1.5rem!important} + .btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}} + .btn:hover{color:#212529} + .btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)} + .btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65} + .btn-primary{color:#fff;background-color:var(--nord3);border-color:var(--nord3)} + .btn-primary:hover{color:#fff;background-color:var(--nord2);border-color:var(--nord2)} + .btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:var(--nord3);border-color:var(--nord3)} + .btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:var(--nord0);border-color:var(--nord0)} + .btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd} + .btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d} + .btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64} + .btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)} + .btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e} + .btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)} + .btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d} + .btn-success{color:#fff;background-color:#198754;border-color:#198754} + .btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43} + .btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)} + .btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f} + .btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)} + .btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754} + .btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0} + .btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2} + .btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)} + .btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2} + .btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)} + .btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0} + .btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107} + .btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720} + .btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)} + .btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720} + .btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)} + .btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107} + .btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545} + .btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37} + .btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)} + .btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834} + .btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)} + .btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545} + .btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa} + .btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb} + .btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)} + .btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb} + .btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)} + .btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa} + .btn-dark{color:#fff;background-color:#212529;border-color:#212529} + .btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21} + .btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)} + .btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f} + .btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)} + .btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529} + .btn-outline-primary{color:#0d6efd;border-color:#0d6efd} + .btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd} + .btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)} + .btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd} + .btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)} + .btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent} + .btn-outline-secondary{color:#6c757d;border-color:#6c757d} + .btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d} + .btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)} + .btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d} + .btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)} + .btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent} + .btn-outline-success{color:#198754;border-color:#198754} + .btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754} + .btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)} + .btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754} + .btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)} + .btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent} + .btn-outline-info{color:#0dcaf0;border-color:#0dcaf0} + .btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0} + .btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)} + .btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0} + .btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)} + .btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent} + .btn-outline-warning{color:#ffc107;border-color:#ffc107} + .btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107} + .btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)} + .btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107} + .btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)} + .btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent} + .btn-outline-danger{color:#dc3545;border-color:#dc3545} + .btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545} + .btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)} + .btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545} + .btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)} + .btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent} + .btn-outline-light{color:#f8f9fa;border-color:#f8f9fa} + .btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa} + .btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)} + .btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa} + .btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)} + .btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent} + .btn-outline-dark{color:#212529;border-color:#212529} + .btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529} + .btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)} + .btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529} + .btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)} + .btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent} + .btn-link{font-weight:400;color:#0d6efd;text-decoration:underline} + .btn-link:hover{color:#0a58ca} + .btn-link.disabled,.btn-link:disabled{color:#6c757d} + .btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem} + .btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem} + .fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}} + .fade:not(.show){opacity:0} + + thead > tr { + background-color: transparent !important; + } + tr:nth-child(even) { + background-color: var(--nord3); + } + tr:nth-child(odd) { + background-color:var(--nord2); + } + + .table-fixed { table-layout: fixed; } + .table-auto { table-layout: auto; } + table.no-overflow>tbody>tr>td { overflow:hidden; text-overflow: ellipsis; white-space: nowrap;} \ No newline at end of file diff --git a/web/overlay/index.js b/web/overlay/index.js index 6b823dc..36059ca 100644 --- a/web/overlay/index.js +++ b/web/overlay/index.js @@ -73,7 +73,7 @@ $(function () { }; - $(document).ready(async ()=>{ + jQuery(async ()=>{ refreshUI(); try{ socket.addFamilyHandler("overlay", handleSocketMessage);