├── .github └── FUNDING.yml ├── .gitignore ├── BUILDING.md ├── LICENSE ├── Old Caffeinated Version Api.tar.gz ├── Old widget.castelrabs.co domain.tar.gz ├── README.md ├── app ├── createWindow.js ├── credits.html ├── css │ ├── quill.css │ ├── selectnsearch.css │ └── style.css ├── index.html ├── js │ ├── analytics.js │ ├── caffeinated.js │ ├── currencies.js │ ├── fontselect.js │ ├── forms.js │ ├── kinoko.js │ ├── koi.js │ ├── lang.js │ ├── modules.js │ ├── navigate.js │ ├── quillutil.js │ ├── repomanager.js │ ├── selectnsearch.js │ └── util.js ├── lang │ ├── en.js │ ├── es.js │ ├── fr.js │ └── nl.js ├── main.js ├── media │ ├── app_icon.icns │ ├── app_icon.ico │ ├── app_icon.png │ ├── caffeinated.png │ └── icon.png ├── modules │ ├── brime │ │ └── brime.js │ ├── caffeine │ │ └── caffeine.js │ ├── ko-fi │ │ └── kofimodule.js │ ├── modules.json │ └── modules │ │ ├── bot.js │ │ ├── chat.js │ │ ├── chatdisplay.html │ │ ├── chatdisplay.js │ │ ├── chatviewers.html │ │ ├── companion.js │ │ ├── donation.js │ │ ├── donationgoal.js │ │ ├── donationticker.js │ │ ├── followcounter.js │ │ ├── follower.js │ │ ├── followergoal.js │ │ ├── nowplaying.js │ │ ├── qr.html │ │ ├── raidalert.js │ │ ├── rain.js │ │ ├── recentdonation.js │ │ ├── recentfollow.js │ │ ├── recentsubscription.js │ │ ├── subscribercounter.js │ │ ├── subscriptionalert.js │ │ ├── subscriptiongoal.js │ │ ├── supporters.js │ │ ├── topdonation.js │ │ ├── uptime.js │ │ ├── videoshare.js │ │ └── viewcounter.js ├── package.json └── uri.all.js.map ├── changelog.html ├── dock ├── dock.js ├── index.html ├── spinkit.css └── style.css ├── package.bat ├── package.sh ├── run.bat ├── run.sh └── widgets ├── alert.html ├── chat.html ├── display.html ├── donation.html ├── emoji.html ├── goal.html ├── nowplaying.html ├── overlayutil.js └── videoshare.html /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: Casterlabs 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *node_modules 2 | *package-lock.json 3 | *dist 4 | *yarn.lock 5 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | https://www.electron.build/multi-platform-build 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Helvijs Adams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Old Caffeinated Version Api.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thehelvijs/Caffeinated/016add60642b88667decd23411605911f865e2f8/Old Caffeinated Version Api.tar.gz -------------------------------------------------------------------------------- /Old widget.castelrabs.co domain.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thehelvijs/Caffeinated/016add60642b88667decd23411605911f865e2f8/Old widget.castelrabs.co domain.tar.gz -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Caffeinated 2 | 3 | We've moved! Check out the new repo [here](https://github.com/Casterlabs/Casterlabs/tree/dev/caffeinated). 4 | -------------------------------------------------------------------------------- /app/createWindow.js: -------------------------------------------------------------------------------- 1 | const { BrowserWindow } = require("electron"); 2 | const windowStateKeeper = require("electron-window-state"); 3 | 4 | function createWindow(baseDir) { 5 | const mainWindowState = windowStateKeeper({ 6 | defaultWidth: 700, 7 | defaultHeight: 500, 8 | file: "main-window.json" 9 | }); 10 | 11 | // Create the browser window. 12 | let mainWindow = new BrowserWindow({ 13 | minWidth: 700, 14 | minHeight: 500, 15 | width: mainWindowState.width, 16 | height: mainWindowState.height, 17 | x: mainWindowState.x, 18 | y: mainWindowState.y, 19 | transparent: false, 20 | resizable: true, 21 | show: false, 22 | backgroundColor: "#141414", 23 | icon: baseDir + "/media/app_icon.png", 24 | frame: false, 25 | webPreferences: { 26 | nodeIntegration: true, 27 | contextIsolation: false, 28 | enableRemoteModule: true, 29 | webSecurity: false 30 | } 31 | }) 32 | 33 | // and load the index.html of the app. 34 | mainWindow.loadFile(baseDir + "/index.html"); 35 | mainWindowState.manage(mainWindow); 36 | 37 | // Emitted when the window is closed. 38 | mainWindow.on("closed", () => { 39 | // Dereference the window object, usually you would store windows 40 | // in an array if your app supports multi windows, this is the time 41 | // when you should delete the corresponding element. 42 | mainWindow = null; 43 | }); 44 | 45 | // Emitted when the window is ready to be shown 46 | // This helps in showing the window gracefully. 47 | mainWindow.once("ready-to-show", () => { 48 | mainWindow.show(); 49 | }); 50 | 51 | return mainWindow; 52 | } 53 | 54 | module.exports = createWindow; -------------------------------------------------------------------------------- /app/css/quill.css: -------------------------------------------------------------------------------- 1 | .rich-editor { 2 | height: 75px; 3 | } 4 | 5 | .ql-snow .ql-stroke { 6 | stroke: whitesmoke; 7 | } 8 | 9 | .ql-snow .ql-fill, .ql-snow .ql-stroke.ql-fill { 10 | fill: whitesmoke; 11 | } 12 | 13 | .ql-picker-label { 14 | color: whitesmoke; 15 | } 16 | 17 | .ql-picker.ql-size .ql-picker-label::before, .ql-picker.ql-size .ql-picker-item::before { 18 | content: attr(data-value) !important; 19 | } 20 | 21 | .ql-color .ql-picker-options [data-value=custom-color], .ql-background .ql-picker-options [data-value=custom-color] { 22 | background: none !important; 23 | width: 100% !important; 24 | height: 20px !important; 25 | text-align: center; 26 | } 27 | 28 | .ql-color .ql-picker-options [data-value=custom-color]:before, .ql-background .ql-picker-options [data-value=custom-color]:before { 29 | content: "Custom Color"; 30 | } 31 | 32 | .ql-color .ql-picker-options [data-value=custom-color]:hover, .ql-background .ql-picker-options [data-value=custom-color]:hover { 33 | border-color: transparent !important; 34 | } -------------------------------------------------------------------------------- /app/css/selectnsearch.css: -------------------------------------------------------------------------------- 1 | 2 | .sns-container { 3 | position: relative; 4 | } 5 | 6 | .sns-contents { 7 | width: 100%; 8 | height: 500%; 9 | overflow-y: scroll; 10 | position: absolute; 11 | background-color: white; 12 | z-index: 99999; 13 | } 14 | 15 | .sns-contents a { 16 | color: black; 17 | width: 100%; 18 | } 19 | 20 | .sns-input { 21 | width: 100%; 22 | } 23 | -------------------------------------------------------------------------------- /app/js/analytics.js: -------------------------------------------------------------------------------- 1 | // Woopra Tracking 2 | !function () { var a, b, c, d = window, e = document, f = arguments, g = "script", h = ["config", "track", "trackForm", "trackClick", "identify", "visit", "push", "call"], i = function () { var a, b = this, c = function (a) { b[a] = function () { return b._e.push([a].concat(Array.prototype.slice.call(arguments, 0))), b } }; for (b._e = [], a = 0; a < h.length; a++)c(h[a]) }; for (d.__woo = d.__woo || {}, a = 0; a < f.length; a++)d.__woo[f[a]] = d[f[a]] = d[f[a]] || new i; b = e.createElement(g), b.async = 1, b.src = "https://static.woopra.com/js/w.js", c = e.getElementsByTagName(g)[0], c.parentNode.insertBefore(b, c) }("woopra"); 3 | 4 | woopra.config({ 5 | domain: "caffeinated.casterlabs.co", 6 | protocol: "https" 7 | }); 8 | 9 | const ANALYTICS = (() => { 10 | let hasLoggedSignin = false; 11 | let hasTracked = false; 12 | 13 | let logSignin = false; 14 | 15 | return { 16 | async logSignin() { 17 | logSignin = true; 18 | }, 19 | 20 | async logSignout() { 21 | woopra.track("signout", {}); 22 | 23 | hasLoggedSignin = false; 24 | hasTracked = false; 25 | 26 | woopra.visitorData = {}; 27 | }, 28 | 29 | async logPuppetSignin() { 30 | woopra.track("puppet_signin", {}); 31 | }, 32 | 33 | async logPuppetSignout() { 34 | woopra.track("puppet_signout", {}); 35 | }, 36 | 37 | async logUserUpdate(userdata = CAFFEINATED.userdata) { 38 | const language = LANG.getTranslation("meta.language.name.native"); 39 | const id = `${userdata.streamer.UUID};${userdata.streamer.platform}`; 40 | const platform = userdata.streamer.platform; 41 | const name = `${userdata.streamer.displayname} (${prettifyString(platform.toLowerCase())})`; 42 | 43 | woopra.identify({ 44 | id: id, 45 | platform: platform, 46 | name: name, 47 | language: language 48 | }); 49 | 50 | if (!hasTracked) { 51 | hasTracked = true; 52 | woopra.track(); 53 | } 54 | 55 | if (logSignin) { 56 | logSignin = false; 57 | 58 | woopra.track("signin", {}); 59 | } 60 | 61 | woopra.push(); 62 | }, 63 | 64 | async logEvent(event) { 65 | if (event && !event.isTest) { 66 | switch (event.event_type) { 67 | case "STREAM_STATUS": { 68 | if (CAFFEINATED.streamdata && (event.title != CAFFEINATED.streamdata.title)) { 69 | woopra.track("stream_title_update", { 70 | title: event.title 71 | }); 72 | } 73 | 74 | if ( 75 | // If there is existing stream data and it doesn't equal the previous state. 76 | (CAFFEINATED.streamdata && (event.is_live != CAFFEINATED.streamdata.is_live)) || 77 | // OR if there isn't existing stream data and the person is live (We should log it.) 78 | (!CAFFEINATED.streamdata && event.is_live) 79 | ) { 80 | if (event.is_live) { 81 | woopra.track("stream_online", {}); 82 | } else { 83 | woopra.track("stream_offline", {}); 84 | } 85 | } 86 | break; 87 | } 88 | } 89 | } 90 | } 91 | 92 | }; 93 | })(); 94 | -------------------------------------------------------------------------------- /app/js/fontselect.js: -------------------------------------------------------------------------------- 1 | const FONTSELECT = { 2 | version: "1.0.1", 3 | endPoint: "https://www.googleapis.com/webfonts/v1/webfonts?sort=popularity&key=AIzaSyBuFeOYplWvsOlgbPeW8OfPUejzzzTCITM", // TODO cache/proxy from Casterlabs' server 4 | fonts: [], 5 | 6 | preload() { 7 | return new Promise(async (resolve, reject) => { 8 | console.debug("Loading fonts."); 9 | 10 | fetch(this.endPoint).then((response) => response.json()) 11 | .catch(reject) 12 | .then(async (fonts) => { 13 | // let toload = []; 14 | 15 | // Quickly get a list of fonts ready for the caller. 16 | fonts.items.forEach((font) => { 17 | const name = font.family; 18 | 19 | if (!this.fonts.includes(name)) { 20 | this.fonts.push(name); 21 | // toload.push(font); 22 | } 23 | }); 24 | 25 | /* 26 | for (const font of toload) { 27 | const name = font.family; 28 | let url; 29 | 30 | if (font.files.hasOwnProperty("regular")) { 31 | url = font.files.regular; 32 | } else { 33 | url = Object.entries(font.files)[0][1]; // Get the entries, get the first entry, get the link. 34 | } 35 | 36 | const face = new FontFace(name, "url(" + url.replace("http:", "https:") + ")"); 37 | 38 | document.fonts.add(face); 39 | } 40 | */ 41 | 42 | try { 43 | const localFonts = await require("font-list").getFonts(); 44 | 45 | localFonts.forEach((font) => { 46 | const name = font.replace(/\"/g, ""); 47 | 48 | if (!this.fonts.includes(name)) { 49 | this.fonts.push(name); 50 | 51 | // const face = new FontFace(name, font); 52 | 53 | // document.fonts.add(face); 54 | } 55 | }) 56 | } catch (e) { 57 | console.error(e); 58 | } 59 | 60 | console.debug("Finished loading fonts."); 61 | resolve(); 62 | }); 63 | }); 64 | }, 65 | 66 | apply(element, settings = { updateFont: true, selected: "Poppins" }) { 67 | return new Promise(async (resolve, reject) => { 68 | // if (element instanceof HTMLSelectElement) { 69 | if (this.fonts.length == 0) { 70 | console.debug("No fonts present, loading them now."); 71 | await this.preload(); 72 | } 73 | 74 | SELECTNSEARCH.create(this.fonts, element); 75 | 76 | element.value = settings.selected; 77 | element.querySelector(".sns-input").setAttribute("value", settings.selected); 78 | 79 | resolve(); 80 | //} else { 81 | // reject("Element is not a valid select element"); 82 | //} 83 | }); 84 | } 85 | 86 | }; 87 | -------------------------------------------------------------------------------- /app/js/forms.js: -------------------------------------------------------------------------------- 1 | // https://github.com/e3ndr/FormsJS/blob/master/forms.js - MIT 2 | const FORMSJS = { 3 | // 1.1.0 MODIFIED HEAVILY 4 | 5 | CLASS_SELECTOR: "data", 6 | NAME_PROPERTY: "name", 7 | BLANK_IS_NULL: false, 8 | PARSE_NUMBERS: false, 9 | ALLOW_FALSE: true, 10 | 11 | readForm(selector, query = document, parent = query.querySelector(selector)) { 12 | let values = {}; 13 | 14 | Array.from(parent.querySelectorAll("." + this.CLASS_SELECTOR)).forEach((element) => { 15 | let name = element.getAttribute(this.NAME_PROPERTY); 16 | 17 | if (name && !values[name]) { 18 | let value = this.getElementValue(element); 19 | 20 | if (this.BLANK_IS_NULL && (value != null) && (value.length == 0)) { 21 | value = null; 22 | } else if ((value === false) && !this.ALLOW_FALSE) { 23 | return; 24 | } else if (this.PARSE_NUMBERS) { 25 | let num = parseFloat(value); 26 | 27 | if (!isNaN(num)) { 28 | value = num; 29 | } 30 | } 31 | 32 | values[name] = value; 33 | } 34 | }); 35 | 36 | return values; 37 | }, 38 | 39 | getElementValue(element) { 40 | let type = element.getAttribute("type"); 41 | 42 | switch (type) { 43 | case "radio": { 44 | if (element.checked) { 45 | return element.value; 46 | } 47 | } 48 | 49 | case "dynamic": { 50 | let options = []; 51 | 52 | Array.from(element.querySelectorAll(".dynamic-option")).forEach((dyn) => { 53 | options.push(FORMSJS.readForm(null, element, dyn)); 54 | }); 55 | 56 | return options; 57 | } 58 | 59 | case "checkbox": 60 | return element.checked; 61 | 62 | case "rich": 63 | return element.lastChild.firstChild.innerHTML; 64 | 65 | case "file": 66 | return element; 67 | 68 | case "currency": 69 | return (element.getAttribute("value").toUpperCase() == "DEFAULT") ? "DEFAULT" : CURRENCY_TABLE[element.getAttribute("value")]; 70 | 71 | case "number": 72 | return parseFloat(element.value); 73 | 74 | default: 75 | return (element instanceof HTMLDivElement) ? element.getAttribute("value") : element.value; 76 | } 77 | } 78 | 79 | }; 80 | -------------------------------------------------------------------------------- /app/js/kinoko.js: -------------------------------------------------------------------------------- 1 | 2 | class Kinoko { 3 | 4 | constructor(baseUri = "wss://api.casterlabs.co/v1/kinoko") { 5 | this.listeners = {}; 6 | this.baseUri = baseUri; 7 | } 8 | 9 | on(type, callback) { 10 | type = type.toLowerCase(); 11 | 12 | let callbacks = this.listeners[type]; 13 | 14 | if (!callbacks) callbacks = []; 15 | 16 | callbacks.push(callback); 17 | 18 | this.listeners[type] = callbacks; 19 | } 20 | 21 | broadcast(type, data) { 22 | const listeners = this.listeners[type.toLowerCase()]; 23 | 24 | if (listeners) { 25 | listeners.forEach((callback) => { 26 | try { 27 | callback(data); 28 | } catch (e) { 29 | console.error("An event listener produced an exception: "); 30 | console.error(e); 31 | } 32 | }); 33 | } 34 | } 35 | 36 | disconnect() { 37 | if (this.ws && (this.ws.readyState == WebSocket.OPEN)) { 38 | this.ws.close(); 39 | } 40 | } 41 | 42 | send(message, isJson = true) { 43 | if (this.ws && (this.ws.readyState == WebSocket.OPEN)) { 44 | if (this.proxy) { 45 | this.ws.send(message); 46 | } else { 47 | if (isJson) { 48 | this.ws.send(JSON.stringify(message)); 49 | } else { 50 | this.ws.send(message); 51 | } 52 | } 53 | } 54 | } 55 | 56 | connect(channel, type = "client", proxy = false) { 57 | setTimeout(() => { 58 | const uri = this.baseUri + "?channel=" + encodeURIComponent(channel) + "&type=" + encodeURIComponent(type) + "&proxy=" + encodeURIComponent(proxy); 59 | 60 | this.disconnect(); 61 | 62 | this.ws = new WebSocket(uri); 63 | this.proxy = proxy; 64 | 65 | this.ws.onerror = () => { 66 | this.connect(channel, type, proxy); 67 | } 68 | 69 | this.ws.onopen = () => { 70 | this.broadcast("open"); 71 | }; 72 | 73 | this.ws.onclose = () => { 74 | this.broadcast("close"); 75 | }; 76 | 77 | this.ws.onmessage = (message) => { 78 | const data = message.data; 79 | 80 | switch (data) { 81 | case ":ping": { 82 | if (!this.proxy) { 83 | this.ws.send(":ping"); 84 | return; 85 | } 86 | } 87 | 88 | case ":orphaned": { 89 | this.broadcast("orphaned"); 90 | return; 91 | } 92 | 93 | case ":adopted": { 94 | this.broadcast("adopted"); 95 | return; 96 | } 97 | 98 | default: { 99 | if (this.proxy) { 100 | this.broadcast("message", data); 101 | } else { 102 | try { 103 | this.broadcast("message", JSON.parse(data)); 104 | } catch (ignored) { 105 | this.broadcast("message", data); 106 | } 107 | } 108 | return 109 | } 110 | } 111 | }; 112 | }, 1500); 113 | } 114 | 115 | } 116 | 117 | // Basically https://stackoverflow.com/a/8809472 118 | function generateUUID() { 119 | let micro = (performance && performance.now && (performance.now() * 1000)) || 0; 120 | let millis = new Date().getTime(); 121 | 122 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { 123 | let random = Math.random() * 16; 124 | 125 | if (millis > 0) { 126 | random = (millis + random) % 16 | 0; 127 | millis = Math.floor(millis / 16); 128 | } else { 129 | random = (micro + random) % 16 | 0; 130 | micro = Math.floor(micro / 16); 131 | } 132 | 133 | return ((c === "x") ? random : ((random & 0x3) | 0x8)).toString(16); 134 | }); 135 | } 136 | 137 | function generateUnsafePassword(len = 32) { 138 | const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 139 | 140 | return Array(len) 141 | .fill(chars) 142 | .map((x) => { 143 | return x[Math.floor(Math.random() * x.length)] 144 | }).join(""); 145 | } 146 | 147 | function generateUnsafeUniquePassword(len = 32) { 148 | return generateUUID().replace(/-/g, "") + generateUnsafePassword(len); 149 | } 150 | 151 | class AuthCallback { 152 | 153 | constructor(type = "unknown") { 154 | this.id = `auth_redirect:${generateUnsafePassword(128)}:${type}`; 155 | } 156 | 157 | disconnect() { 158 | if (this.kinoko) { 159 | this.kinoko.disconnect(); 160 | } 161 | 162 | this.kinoko = new Kinoko(); 163 | } 164 | 165 | awaitAuthMessage(timeout = -1) { 166 | return new Promise((resolve, reject) => { 167 | this.disconnect(); 168 | 169 | let fufilled = false; 170 | const id = (timeout > 0) ? setTimeout(() => { 171 | if (!fufilled) { 172 | fufilled = true; 173 | this.disconnect(); 174 | reject("TOKEN_TIMEOUT"); 175 | } 176 | }, timeout) : -1; 177 | 178 | this.kinoko.connect(this.id, "parent"); 179 | 180 | this.kinoko.on("close", () => { 181 | if (!fufilled) { 182 | reject("CONNECTION_CLOSED"); 183 | } 184 | 185 | clearTimeout(id); 186 | }); 187 | 188 | this.kinoko.on("message", (message) => { 189 | fufilled = true; 190 | 191 | this.disconnect(); 192 | 193 | if (message === "NONE") { 194 | reject("NO_TOKEN_PROVIDED"); 195 | } else if (message.startsWith("token:")) { 196 | const token = message.substring(6); 197 | 198 | resolve(token); 199 | } else { 200 | reject("TOKEN_MESSAGE_INVALID"); 201 | } 202 | }); 203 | }); 204 | } 205 | 206 | getStateString() { 207 | return this.id; 208 | } 209 | 210 | } 211 | -------------------------------------------------------------------------------- /app/js/lang.js: -------------------------------------------------------------------------------- 1 | let LANGUAGES = {}; 2 | 3 | const LANG = { 4 | supportedLanguages: {}, 5 | 6 | absorbLang(newLang, code) { 7 | const languageName = newLang["meta.language.name.native"]; 8 | 9 | this.supportedLanguages[languageName] = code; 10 | 11 | for (const [key, value] of Object.entries(newLang)) { 12 | if (!LANGUAGES[key]) { 13 | LANGUAGES[key] = {}; 14 | } 15 | 16 | LANGUAGES[key][code] = value; 17 | } 18 | }, 19 | 20 | getLangKey(key, language = navigator.language) { 21 | const lang = LANGUAGES[key]; 22 | 23 | if (lang) { 24 | for (const code of Object.keys(lang)) { 25 | const regex = RepoUtil.matchToRegex(code); 26 | 27 | if (language.match(regex)) { 28 | return code; 29 | } 30 | } 31 | } 32 | 33 | return null; 34 | }, 35 | 36 | getSupportedLanguage(key, languages = navigator.languages) { 37 | const stored = CAFFEINATED.store.get("language"); 38 | 39 | if (stored) { 40 | const code = this.getLangKey(key, stored); 41 | 42 | if (code) { 43 | return code; 44 | } else if (CAFFEINATED.store.get("experimental.no_translation_default")) { 45 | return ""; 46 | } 47 | // Otherwise, figure it out based on what the OS gives us. 48 | } 49 | 50 | for (const lang of languages) { 51 | const code = this.getLangKey(key, lang); 52 | 53 | if (code) { 54 | return code; 55 | } 56 | } 57 | 58 | if (CAFFEINATED.store.get("experimental.no_translation_default")) { 59 | return ""; 60 | } else { 61 | return "en-*"; 62 | } 63 | }, 64 | 65 | translate(parent = document, ...args) { 66 | Array.from(parent.querySelectorAll(".translatable")).forEach((element) => { 67 | const key = element.getAttribute("lang"); 68 | const lang = LANGUAGES[key]; 69 | 70 | let result; 71 | 72 | if (lang) { 73 | const supported = this.getSupportedLanguage(key); 74 | let translated = supported ? lang[supported] : key; 75 | 76 | if (translated === undefined) { 77 | result = key; 78 | } else { 79 | if (typeof translated === "function") { 80 | result = translated(...args); 81 | } else { 82 | result = translated; 83 | } 84 | } 85 | } else { 86 | result = key; 87 | } 88 | 89 | element.innerText = result; 90 | element.setAttribute("title", result); 91 | }); 92 | }, 93 | 94 | getTranslation(key, ...args) { 95 | const lang = LANGUAGES[key]; 96 | 97 | if (lang) { 98 | const supported = this.getSupportedLanguage(key); 99 | let translated = supported ? lang[supported] : key; 100 | 101 | if (translated === undefined) { 102 | return key; 103 | } else { 104 | if (typeof translated === "function") { 105 | translated = translated(...args); 106 | } 107 | 108 | return translated; 109 | } 110 | } else { 111 | return key; 112 | } 113 | }, 114 | 115 | formatSubscription(event) { 116 | const months = `${event.months}`; 117 | 118 | switch (event.sub_type) { 119 | case "SUB": 120 | return this.getTranslation("caffeinated.subscription_alert.format.sub", `${event.subscriber.displayname}`, months); 121 | 122 | case "RESUB": 123 | return this.getTranslation("caffeinated.subscription_alert.format.resub", `${event.subscriber.displayname}`, months); 124 | 125 | case "SUBGIFT": 126 | return this.getTranslation("caffeinated.subscription_alert.format.subgift", `${event.subscriber.displayname}`, `${event.gift_recipient.displayname}`, months); 127 | 128 | case "RESUBGIFT": 129 | return this.getTranslation("caffeinated.subscription_alert.format.resubgift", `${event.subscriber.displayname}`, `${event.gift_recipient.displayname}`, months); 130 | 131 | case "ANONSUBGIFT": 132 | return this.getTranslation("caffeinated.subscription_alert.format.anonsubgift", `${event.gift_recipient.displayname}`, months); 133 | 134 | case "ANONRESUBGIFT": 135 | return this.getTranslation("caffeinated.subscription_alert.format.anonresubgift", `${event.gift_recipient.displayname}`, months); 136 | 137 | } 138 | } 139 | 140 | }; 141 | -------------------------------------------------------------------------------- /app/js/navigate.js: -------------------------------------------------------------------------------- 1 | 2 | function navigate(page) { 3 | let selector = "[page='" + page + "']"; 4 | 5 | // Check if already active page is navigated (to stop double loading content) 6 | let isActive = false; 7 | Array.from(document.querySelectorAll(".page")).forEach((e) => { 8 | if (!(e.classList.contains("hide")) && e.getAttribute("page") == page) { 9 | isActive = true; 10 | } 11 | }); 12 | 13 | if (!isActive) { 14 | anime({ 15 | targets: ".page", 16 | easing: "linear", 17 | opacity: 0, 18 | duration: 100 19 | }).finished.then(() => { 20 | Array.from(document.querySelectorAll(".page")).forEach((e) => { 21 | if (e.getAttribute("page") != page) { 22 | e.classList.add("hide"); 23 | // Remove active class from menu button 24 | try { 25 | document.getElementById("menu-" + e.getAttribute("page")).classList.remove("active"); 26 | } catch (e) { } 27 | } 28 | }); 29 | 30 | // Sets menu button active class 31 | // Try/catch in case if 'navigate' is triggered from outside side-menu 32 | try { 33 | document.getElementById("menu-" + page).classList.add("active"); 34 | } catch (e) { } 35 | 36 | document.querySelector(selector).classList.remove("hide"); 37 | 38 | anime({ 39 | targets: selector, 40 | easing: "linear", 41 | opacity: 1, 42 | duration: 100 43 | }); 44 | 45 | // (Helvijs) Title is not needed as there is active menu button 46 | // But once menu hiding is added, the title could be on top of content page 47 | // let title = document.querySelector(selector).getAttribute("navbar-title"); 48 | // if (title) { 49 | // document.querySelector(".currentpage").innerText = title; 50 | // } else { 51 | // document.querySelector(".currentpage").innerText = prettifyString(page); 52 | // } 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/js/quillutil.js: -------------------------------------------------------------------------------- 1 | 2 | const QuillUtil = { 3 | fontSizes: ["8px", "9px", "10px", "12px", "14px", "16px", "20px", "24px", "32px", "42px", "54px", "68px", "84px", "98px"], 4 | colors: ["#000000", "#e60000", "#ff9900", "#ffff00", "#008a00", "#0066cc", "#9933ff", "#ffffff", "#facccc", "#ffebcc", "#ffffcc", "#cce8cc", "#cce0f5", "#ebd6ff", "#bbbbbb", "#f06666", "#ffc266", "#ffff66", "#66b966", "#66a3e0", "#c285ff", "#888888", "#a10000", "#b26b00", "#b2b200", "#006100", "#0047b2", "#6b24b2", "#444444", "#5c0000", "#663d00", "#666600", "#003700", "#002966", "#3d1466", "custom-color"], 5 | // fonts: [], // TODO 6 | 7 | getCustomColor() { 8 | return new Promise((resolve) => { 9 | const element = document.createElement("input"); 10 | 11 | element.value = "#e94b4b"; 12 | element.setAttribute("type", "color"); 13 | element.addEventListener("change", () => { 14 | resolve(element.value); 15 | }); 16 | 17 | element.click(); 18 | }); 19 | }, 20 | 21 | createEditor(element, value, callback) { 22 | const inner = document.createElement("div"); 23 | 24 | inner.classList.add("rich-editor"); 25 | 26 | element.appendChild(inner); 27 | 28 | element.addEventListener("DOMNodeInsertedIntoDocument", () => { 29 | const quill = new Quill(inner, { 30 | modules: { 31 | toolbar: { 32 | container: [ 33 | // [{ "font": QuillUtil.fonts }], 34 | [{ "size": QuillUtil.fontSizes }], 35 | [{ "align": [] }], 36 | [{ "color": QuillUtil.colors }, { "background": QuillUtil.colors }], 37 | ["bold", "italic", "underline"], 38 | ["clean"] 39 | ], 40 | handlers: { 41 | "color": (value) => { 42 | if (value == "custom-color") { 43 | QuillUtil.getCustomColor().then((color) => { 44 | quill.format("color", color); 45 | }); 46 | } else { 47 | quill.format("color", value); 48 | } 49 | }, 50 | "background": (value) => { 51 | if (value == "custom-color") { 52 | QuillUtil.getCustomColor().then((color) => { 53 | quill.format("background", color); 54 | }); 55 | } else { 56 | quill.format("background", value); 57 | } 58 | } 59 | } 60 | } 61 | }, 62 | placeholder: "", 63 | theme: "snow" 64 | }); 65 | 66 | quill.format("color", "#FFFFFF"); 67 | quill.root.innerHTML = value; 68 | quill.root.addEventListener("blur", callback); 69 | }); 70 | } 71 | 72 | }; 73 | 74 | (() => { 75 | let quillSize = Quill.import("attributors/style/size"); 76 | quillSize.whitelist = QuillUtil.fontSizes; 77 | Quill.register(quillSize, true); 78 | })(); -------------------------------------------------------------------------------- /app/js/repomanager.js: -------------------------------------------------------------------------------- 1 | class RepoManager { 2 | constructor() { 3 | this.repos = []; 4 | this.elements = []; 5 | } 6 | 7 | async addRepo(repo) { 8 | try { 9 | let modules = await (await fetch(repo + "/modules.json")).json(); 10 | 11 | if (RepoUtil.isSupported(modules.supported, modules.unsupported)) { 12 | if (Array.isArray(modules.preload)) { 13 | for (let required of modules.preload) { 14 | if (required.endsWith("/")) { 15 | required = required.substring(required, required.length - 1); 16 | } 17 | console.log("Repo " + repo + " requires the following repo " + required + ", loading it now."); 18 | await this.addRepo(required, false); 19 | } 20 | } 21 | 22 | if (Array.isArray(modules.scripts)) { 23 | for (let src of modules.scripts) { 24 | let script = document.createElement("script"); 25 | 26 | script.src = repo + "/" + src; 27 | script.setAttribute("repo", modules.name); 28 | 29 | this.elements.push(script); 30 | document.querySelector("#scripts").appendChild(script); 31 | await RepoUtil.waitForScriptToLoad(script); 32 | } 33 | } 34 | 35 | if (Array.isArray(modules.external)) { 36 | for (let external of modules.external) { 37 | let script = document.createElement("script"); 38 | 39 | script.src = external; 40 | script.setAttribute("repo", modules.name); 41 | 42 | this.elements.push(script); 43 | document.querySelector("#scripts").appendChild(script); 44 | await RepoUtil.waitForScriptToLoad(script); 45 | } 46 | } 47 | 48 | if (Array.isArray(modules.simple)) { 49 | for (let instance of modules.simple) { 50 | const namespace = instance.namespace; 51 | const id = instance.id; 52 | 53 | let loaded = await MODULES.getFromUUID(namespace + ":" + id); 54 | 55 | if (!loaded) { 56 | const clazz = MODULES.moduleClasses[namespace] ?? MODULES.uniqueModuleClasses[namespace]; 57 | const module = new clazz(id); 58 | 59 | await MODULES.initalizeModule(module); 60 | } 61 | } 62 | } 63 | 64 | if (Array.isArray(modules.required)) { 65 | for (let instance of modules.required) { 66 | const namespace = instance.namespace; 67 | const id = instance.id; 68 | 69 | let loaded = await MODULES.getFromUUID(namespace + ":" + id); 70 | 71 | if (!loaded) { 72 | const clazz = MODULES.moduleClasses[namespace] ?? MODULES.uniqueModuleClasses[namespace]; 73 | const module = new clazz(id); 74 | 75 | await MODULES.initalizeModule(module); 76 | 77 | module.persist = true; 78 | } 79 | } 80 | } 81 | 82 | this.repos.push(modules); 83 | } else { 84 | throw "Repo " + repo + " doesn't support the current version of caffeinated"; 85 | } 86 | } catch (e) { 87 | console.error(e); 88 | throw "Unable to retrieve modules from " + repo; 89 | }; 90 | } 91 | 92 | } 93 | 94 | const RepoUtil = { 95 | matchToRegex(str) { 96 | return new RegExp("(" + str.replace(/[\.]/g, "\\.").replace(/[\*]/g, ".*") + ")", "g"); 97 | }, 98 | 99 | waitForScriptToLoad(script) { 100 | return new Promise((resolve) => { 101 | script.addEventListener("load", () => resolve()); 102 | }); 103 | }, 104 | 105 | isSupported(supported = [], unsupported = []) { 106 | if (Array.isArray(unsupported)) { 107 | for (let version of unsupported) { 108 | if (VERSION.match(RepoUtil.matchToRegex(version))) { 109 | return false; 110 | } 111 | } 112 | } 113 | 114 | if (Array.isArray(supported)) { 115 | for (let version of supported) { 116 | if (VERSION.match(RepoUtil.matchToRegex(version))) { 117 | return true; 118 | } 119 | } 120 | } 121 | 122 | return false; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/js/selectnsearch.js: -------------------------------------------------------------------------------- 1 | const SELECTNSEARCH = { 2 | 3 | create(contents = [], div = document.createElement("div")) { 4 | const input = document.createElement("input"); 5 | const contentDiv = document.createElement("div"); 6 | 7 | div.appendChild(input); 8 | div.appendChild(contentDiv); 9 | div.classList.add("sns-container"); 10 | 11 | contentDiv.classList.add("sns-contents"); 12 | contentDiv.style.display = "none"; 13 | 14 | input.setAttribute("type", "search"); 15 | input.classList.add("sns-input"); 16 | input.addEventListener("focus", () => { 17 | SELECTNSEARCH.search(div, ""); 18 | SELECTNSEARCH.hideAll(); 19 | contentDiv.style.display = "block"; 20 | }); 21 | document.addEventListener("click", () => { 22 | const selection = getSelection(); 23 | 24 | if (selection && selection.focusNode) { 25 | if ((selection.focusNode.classList == undefined) || !selection.focusNode.classList.contains("sns-container")) { 26 | contentDiv.style.display = "none"; 27 | } 28 | } 29 | }); 30 | input.addEventListener("close", () => { 31 | contentDiv.style.display = "none"; 32 | }); 33 | input.addEventListener("keyup", () => { 34 | SELECTNSEARCH.search(div); 35 | }); 36 | 37 | this.populate(div, contents); 38 | 39 | return div; 40 | }, 41 | 42 | hideAll() { 43 | Array.from(document.querySelectorAll(".sns-contents")).forEach((content) => content.style.display = "none"); 44 | }, 45 | 46 | search(div, value) { 47 | let contentDiv = div.querySelector(".sns-contents"); 48 | let input = div.querySelector(".sns-input"); 49 | 50 | if (value == null) { 51 | value = input.value.toLowerCase().trim(); 52 | } 53 | 54 | Array.from(contentDiv.childNodes).forEach((item) => { 55 | if (item.getAttribute("value").toLowerCase().trim().includes(value)) { 56 | item.style.display = "block"; 57 | 58 | if (!item.getAttribute("displayed")) { 59 | item.setAttribute("displayed", true); 60 | item.dispatchEvent(new CustomEvent("visible")); 61 | } 62 | } else { 63 | item.style.display = "none"; 64 | } 65 | }); 66 | }, 67 | 68 | populate(div, contents) { 69 | let contentDiv = div.querySelector(".sns-contents"); 70 | let input = div.querySelector(".sns-input"); 71 | 72 | contentDiv.innerHTML = ""; 73 | 74 | contents.forEach((section) => { 75 | let item = document.createElement("a"); 76 | let text; 77 | let callback; 78 | 79 | if (typeof section == "string") { 80 | text = section; 81 | } else { 82 | text = secion.text; 83 | callback = section.callback; 84 | } 85 | 86 | item.innerText = text; 87 | item.style.display = "block"; 88 | item.setAttribute("value", text); 89 | item.addEventListener("click", () => { 90 | input.value = text; 91 | div.setAttribute("value", text); 92 | input.dispatchEvent(new CustomEvent("close")); 93 | div.dispatchEvent(new CustomEvent("change")); 94 | }); 95 | 96 | if (callback) { 97 | callback(item); 98 | } 99 | 100 | contentDiv.appendChild(item); 101 | }); 102 | }, 103 | 104 | checkVisible(elm, parent = document.documentElement) { 105 | let rect = elm.getBoundingClientRect(); 106 | let viewHeight = parent.offsetHeight; 107 | 108 | return !(rect.bottom < 0 || rect.top - viewHeight >= 0); 109 | } 110 | 111 | } -------------------------------------------------------------------------------- /app/js/util.js: -------------------------------------------------------------------------------- 1 | const FILE_SIZE_THRESHOLD = 1048576 * 10; // 10mb 2 | 3 | function looseInterpret(code, ...args) { 4 | try { 5 | return Function(code)(...args); 6 | } catch (e) { 7 | if (typeof test === "string") { 8 | alert(`Uncaught Exception: ${e}`); 9 | } else { 10 | alert(`Uncaught ${e.name}: ${e.message}`); 11 | } 12 | } 13 | } 14 | 15 | function openLink(link) { 16 | shell.openExternal(link); 17 | } 18 | 19 | function kFormatter(num, decimalPlaces = 1, threshold = 1000) { 20 | const negative = num < 0; 21 | let shortened; 22 | let mult; 23 | 24 | num = Math.abs(num); 25 | 26 | if ((num >= threshold) && (num >= 1000)) { 27 | if (num >= 1000000000000) { 28 | shortened = "Over 1"; 29 | mult = "t"; 30 | } else if (num >= 1000000000) { 31 | shortened = (num / 1000000000).toFixed(decimalPlaces); 32 | mult = "b"; 33 | } else if (num >= 1000000) { 34 | shortened = (num / 1000000).toFixed(decimalPlaces); 35 | mult = "m"; 36 | } else if (num >= 1000) { 37 | shortened = (num / 1000).toFixed(decimalPlaces); 38 | mult = "k"; 39 | } 40 | } else { 41 | shortened = num.toFixed(decimalPlaces); 42 | mult = ""; 43 | } 44 | 45 | if (shortened.includes(".")) { 46 | shortened = shortened.replace(/\.?0+$/, ''); 47 | } 48 | 49 | return (negative ? "-" : "") + shortened + mult; 50 | } 51 | 52 | function fileSizeFormatter(num, decimalPlaces = 1, threshold = 1000) { 53 | let shortened; 54 | let mult; 55 | 56 | if ((num >= threshold) && (num >= 1000)) { 57 | if (num >= 1099511627776) { 58 | shortened = "Over 1"; 59 | mult = "tb"; 60 | } else if (num >= 1073741824) { 61 | shortened = (num / 1000000000).toFixed(decimalPlaces); 62 | mult = "gb"; 63 | } else if (num >= 1048576) { 64 | shortened = (num / 1000000).toFixed(decimalPlaces); 65 | mult = "mb"; 66 | } else if (num >= 1024) { 67 | shortened = (num / 1000).toFixed(decimalPlaces); 68 | mult = "kb"; 69 | } 70 | } else { 71 | shortened = num.toFixed(decimalPlaces); 72 | mult = "b"; 73 | } 74 | 75 | if (shortened.includes(".")) { 76 | shortened = shortened.replace(/\.?0+$/, ''); 77 | } 78 | 79 | return shortened + mult; 80 | } 81 | 82 | function sleep(millis) { 83 | return new Promise((resolve) => setTimeout(resolve, millis)); 84 | } 85 | 86 | function prettifyString(str) { 87 | let splitStr = str.split("_"); 88 | 89 | if (splitStr.length == 0) { 90 | return splitStr[0].charAt(0).toUpperCase() + splitStr[0].substring(1); 91 | } else { 92 | for (let i = 0; i < splitStr.length; i++) { 93 | splitStr[i] = splitStr[i].charAt(0).toUpperCase() + splitStr[i].substring(1); 94 | } 95 | 96 | return splitStr.join(" "); 97 | } 98 | } 99 | 100 | function putInClipboard(copy) { 101 | navigator.clipboard.writeText(copy); 102 | } 103 | 104 | function fileToBase64(fileElement, type) { 105 | return new Promise((resolve) => { 106 | const file = fileElement.files[0]; 107 | const size = file.size; 108 | 109 | if (size > FILE_SIZE_THRESHOLD) { 110 | if (confirm(`The current selected file is greater than 10mb (Actual Size: ${fileSizeFormatter(size, 1)}) which is known to cause issues with Caffeinated.\n\nEither click OK to proceed or click cancel to select a smaller file.`)) { 111 | console.debug("User OK'd a large file read."); 112 | } else { 113 | resolve(""); 114 | fileElement.value = ""; 115 | console.debug("User aborted a large file read."); 116 | return; 117 | } 118 | } 119 | 120 | console.debug(`Reading a ${fileSizeFormatter(size, 1)} file.`) 121 | 122 | try { 123 | const reader = new FileReader(); 124 | 125 | reader.readAsDataURL(file); 126 | reader.onload = () => { 127 | const result = reader.result; 128 | 129 | fileElement.value = ""; 130 | 131 | if (!type || result.startsWith("data:" + type)) { 132 | resolve(result); 133 | } else { 134 | resolve(""); 135 | } 136 | } 137 | } catch (e) { 138 | console.warn(e); 139 | resolve(""); 140 | fileElement.value = ""; 141 | } 142 | }); 143 | } 144 | 145 | function playAudio(b64, vol = 1) { 146 | try { 147 | let audio = new Audio(b64); 148 | 149 | audio.volume = vol; 150 | audio.play(); 151 | 152 | return audio; 153 | } catch (e) { 154 | return {}; 155 | } 156 | } 157 | 158 | function nullFields(object, fields) { 159 | let clone = Object.assign({}, object); 160 | 161 | fields.forEach((field) => { 162 | clone[field] = null; 163 | }); 164 | 165 | return clone; 166 | } 167 | 168 | function removeFromArray(array, item) { 169 | const index = array.indexOf(item); 170 | 171 | if (index > -1) { 172 | array.splice(index, 1); 173 | } 174 | 175 | return array; 176 | } 177 | 178 | function escapeHtml(unsafe) { 179 | return unsafe 180 | .replace(/&/g, "&") 181 | .replace(//g, ">"); 183 | } 184 | 185 | function getFriendlyTime(millis) { 186 | return new Date(millis).toISOString().substr(11, 8); 187 | } 188 | -------------------------------------------------------------------------------- /app/lang/en.js: -------------------------------------------------------------------------------- 1 | LANG.absorbLang({ 2 | "meta.language.name": "English", 3 | "meta.language.name.native": "English", 4 | "meta.language.code": "en-*", 5 | 6 | // UI 7 | "caffeinated.internal.widgets": "Widgets", 8 | "caffeinated.internal.followers_count_text": (count) => `${count} ${(count == 1 ? "follower" : "followers")}`, 9 | "caffeinated.internal.subscribers_count_text": (count) => `${count} ${(count == 1 ? "subscriber" : "subscribers")}`, 10 | 11 | // Generic 12 | "generic.enabled": "Enabled", 13 | "generic.font": "Font", 14 | "generic.font.size": "Font Size (px)", 15 | "generic.text.color": "Text Color", 16 | "generic.background.color": "Background Color", 17 | "generic.volume": "Volume", 18 | "generic.alert.audio": "Alert Audio", 19 | "generic.alert.image": "Alert Image", 20 | "generic.audio.file": "Audio File", 21 | "generic.image.file": "Image File", 22 | "generic.currency": "Currency", 23 | "generic.enable_audio": "Enable Custom Audio", 24 | "generic.use_custom_image": "Use Custom Image", 25 | "generic.height": "Height (px)", 26 | "generic.height": "Width (px)", 27 | 28 | // Video Share 29 | "caffeinated.videoshare.title": "Video Share", 30 | "caffeinated.videoshare.donations_only": "Donations Only", 31 | "caffeinated.videoshare.skip": "Skip", 32 | "caffeinated.videoshare.pause": "Play/Pause", 33 | "caffeinated.videoshare.player_only": "Player Only (No frame)", 34 | 35 | // Raid 36 | "caffeinated.raid_alert.title": "Raid Alert", 37 | "caffeinated.raid_alert.format.now_raiding": (raider, viewers) => `${raider} just raided with ${viewers} ${(viewers == 1) ? "viewer" : "viewers"}`, 38 | 39 | // Subscription Goal 40 | "caffeinated.subscription_goal.title": "Subscription Goal", 41 | 42 | // Subscription 43 | "caffeinated.subscription_alert.title": "Subscription Alert", 44 | "caffeinated.subscription_alert.format.sub": (name, months) => `${name} just subscribed for ${months} ${(months == 1) ? "month" : "months"}`, 45 | "caffeinated.subscription_alert.format.resub": (name, months) => `${name} just resubscribed for ${months} ${(months == 1) ? "month" : "months"}`, 46 | "caffeinated.subscription_alert.format.subgift": (name, giftee, months) => `${name} just gifted ${giftee} a ${months} month subscription`, 47 | "caffeinated.subscription_alert.format.resubgift": (name, giftee, months) => `${name} just gifted ${giftee} a ${months} month resubscription`, 48 | "caffeinated.subscription_alert.format.anonsubgift": (giftee, months) => `Anonymous just gifted ${giftee} a ${months} month subscription`, 49 | "caffeinated.subscription_alert.format.anonresubgift": (giftee, months) => `Anonymous just gifted ${giftee} a ${months} month resubscription`, 50 | 51 | // Credits 52 | "caffeinated.credits.title": "Credits", 53 | 54 | // Settings 55 | "caffeinated.settings.title": "Settings", 56 | "caffeinated.settings.signout": "Sign out", 57 | "caffeinated.settings.language": "Language", 58 | "caffeinated.settings.view_changelog": "View Changelog", 59 | "caffeinated.settings.chatbot_login": "Link chatbot account", 60 | "caffeinated.settings.chatbot_logout": "Unlink chatbot account", 61 | "caffeinated.settings.enable_discord_integration": "Enable Discord Integration", 62 | 63 | // Stream Uptime 64 | "caffeinated.uptime.title": "Stream Uptime", 65 | 66 | // Support Us 67 | "caffeinated.supporters.title": "Support Us", 68 | 69 | // Chat Display 70 | "caffeinated.chatdisplay.title": "Chat", 71 | "caffeinated.chatdisplay.join_text": (name) => `${name} joined the stream`, 72 | "caffeinated.chatdisplay.leave_text": (name) => `${name} left the stream`, 73 | "caffeinated.chatdisplay.follow_text": (name) => `${name} started following`, 74 | "caffeinated.chatdisplay.reward_text": (name, title, image) => `${name} just redeemed ${image}${title}`, 75 | "caffeinated.chatdisplay.show_viewers": "Show Viewers", 76 | "caffeinated.chatdisplay.copy_chat_dock_link": "Copy Chat OBS Dock Link", 77 | "caffeinated.chatdisplay.copy_viewers_dock_link": "Copy Viewers List OBS Dock Link", 78 | 79 | // Chat 80 | "caffeinated.chat.title": "Chat", 81 | "caffeinated.chat.show_donations": "Show Donations", 82 | "caffeinated.chat.chat_direction": "Direction", 83 | "caffeinated.chat.chat_animation": "Animation", 84 | "caffeinated.chat.text_align": "Text Align", 85 | 86 | // Donation Goal 87 | "caffeinated.donation_goal.title": "Donation Goal", 88 | "caffeinated.donation_goal.current_amount": "Current Amount", 89 | 90 | // Follower Goal 91 | "caffeinated.follower_goal.title": "Follower Goal", 92 | 93 | // Generic Goal 94 | "caffeinated.generic_goal.name": "Title", 95 | "caffeinated.generic_goal.goal_amount": "Target Amount", 96 | "caffeinated.generic_goal.text_color": "Title Color", 97 | "caffeinated.generic_goal.bar_color": "Progress Bar Color", 98 | 99 | // Donation Alert 100 | "caffeinated.donation_alert.title": "Donation Alert", 101 | "caffeinated.donation_alert.text_to_speech_voice": "TTS Voice", 102 | 103 | // Follower Alert 104 | "caffeinated.follower_alert.title": "Follower Alert", 105 | "caffeinated.follower_alert.format.followed": (user) => `${user} just followed`, 106 | 107 | // Spotify 108 | "spotify.integration.title": "Spotify", 109 | "spotify.integration.login": "Login with Spotify", 110 | "spotify.integration.logging_in": "Logging in", 111 | "spotify.integration.logged_in_as": (name) => `Logged in as ${name} (Click to log out)`, 112 | "spotify.integration.announce": "Announce Song", 113 | "spotify.integration.enable_song_command": "Enable Song Command", 114 | "spotify.integration.background_style": "Background Style", 115 | "spotify.integration.image_style": "Image Style", 116 | "spotify.integration.now_playing_announcment": (title, artist) => `Now playing: ${title} - ${artist}`, 117 | 118 | // View Counter 119 | "caffeinated.view_counter.title": "View Counter", 120 | 121 | // Recent Follow 122 | "caffeinated.recent_follow.title": "Recent Follow", 123 | 124 | // Donation Ticker 125 | "caffeinated.donation_ticker.title": "Donation Ticker", 126 | 127 | // Top Donation 128 | "caffeinated.top_donation.title": "Top Donation", 129 | 130 | // Recent Donation 131 | "caffeinated.recent_donation.title": "Recent Donation", 132 | 133 | // Recent Subscription 134 | "caffeinated.recent_subscription.title": "Recent Subscription", 135 | 136 | // Follow Counter 137 | "caffeinated.follow_counter.title": "Follow Counter", 138 | 139 | // Subscriber Counter 140 | "caffeinated.subscriber_counter.title": "Subscriber Counter", 141 | 142 | // Chat Bot 143 | "caffeinated.chatbot.title": "Chat Bot", 144 | "caffeinated.chatbot.commands": "Commands", 145 | "caffeinated.chatbot.follow_callout": "Follow Callout (Leave blank to disable)", 146 | "caffeinated.chatbot.donation_callout": "Donation Callout (Leave blank to disable)", 147 | "caffeinated.chatbot.welcome_callout": "Welcome Callout (Leave blank to disable)", 148 | "caffeinated.chatbot.default_reply": "Casterlabs is a free stream widget service!", 149 | "caffeinated.chatbot.command_type": "Command Type", 150 | "caffeinated.chatbot.trigger": "Trigger", 151 | "caffeinated.chatbot.reply": "Reply", 152 | "caffeinated.chatbot.uptime_command.enable": "Enable Uptime Command", 153 | "caffeinated.chatbot.uptime_command.format": (time) => `The stream has been up for ${time}`, 154 | "caffeinated.chatbot.uptime_command.not_live": "We're off the air", 155 | 156 | // Caffeine Integration 157 | "caffeine.integration.title": "Caffeine", 158 | "caffeine.integration.new_thumbnail": "New thumbnail", 159 | "caffeine.integration.rating_selector": "Content Rating", 160 | "caffeine.integration.title_selector": "Title", 161 | "caffeine.integration.game_selector": "Selected Game", 162 | "caffeine.integration.update": "Update", 163 | 164 | // Casterlabs Companion 165 | "caffeinated.companion.title": "Casterlabs Companion", 166 | "caffeinated.companion.copy": "Copy Link", 167 | "caffeinated.companion.reset": "Reset Link" 168 | 169 | }, "en-*"); 170 | -------------------------------------------------------------------------------- /app/lang/es.js: -------------------------------------------------------------------------------- 1 | LANG.absorbLang({ 2 | "meta.language.name": "Spanish", 3 | "meta.language.name.native": "Español", 4 | "meta.language.code": "es-*", 5 | 6 | // UI 7 | "caffeinated.internal.widgets": "Widgets", 8 | "caffeinated.internal.followers_count_text": (count) => `${count} ${(count == 1 ? "seguidor(a)" : "seguidores")}`, 9 | "caffeinated.internal.subscribers_count_text": (count) => `${count} ${(count == 1 ? "abonado" : "suscriptores")}`, 10 | 11 | // Generic 12 | "generic.enabled": "Activado", 13 | "generic.font": "Fuente", 14 | "generic.font.size": "Tamaño de Fuente (px)", 15 | "generic.text.color": "Color del Texto", 16 | // TODO "generic.background.color": "Background Color", 17 | "generic.volume": "Volumen", 18 | "generic.alert.audio": "Audio de Alerta", 19 | "generic.alert.image": "Imagen de Alerta", 20 | "generic.audio.file": "Archivo de Audio", 21 | "generic.image.file": "Archivo de Imagen", 22 | "generic.currency": "Moneda", 23 | "generic.enable_audio": "Activar audio personalizado", 24 | "generic.use_custom_image": "Utilizar imagen personalizada", 25 | // TODO "generic.height": "Height (px)", 26 | // TODO "generic.height": "Width (px)", 27 | 28 | // Video Share 29 | // TODO "caffeinated.videoshare.title": "Video Share", 30 | // TODO "caffeinated.videoshare.donations_only": "Donations Only", 31 | // TODO "caffeinated.videoshare.skip": "Skip", 32 | // TODO "caffeinated.videoshare.pause": "Play/Pause", 33 | // TODO "caffeinated.videoshare.player_only": "Player Only (No frame)", 34 | 35 | // Raid 36 | "caffeinated.raid_alert.title": "Alerta de Incursión", 37 | "caffeinated.raid_alert.format.now_raiding": (raider, viewers) => `${raider} acaba de llegar con ${viewers} espectadores`, 38 | 39 | // Subscription Goal 40 | "caffeinated.subscription_goal.title": "Meta de Suscripciónes", 41 | 42 | // Subscription 43 | "caffeinated.subscription_alert.title": "Alerta de Suscripción", 44 | "caffeinated.subscription_alert.format.sub": (name, months) => `${name} acaba de suscribir por ${months} meses`, 45 | "caffeinated.subscription_alert.format.resub": (name, months) => `${name} acab de resubscribir por ${months} meses`, 46 | "caffeinated.subscription_alert.format.subgift": (name, giftee, months) => `${name} le acaba de regalar a ${giftee} una suscripción de ${months} meses`, 47 | "caffeinated.subscription_alert.format.resubgift": (name, giftee, months) => `${name} le acaba de regalar a ${giftee} una sreuscripción de ${months} meses`, 48 | "caffeinated.subscription_alert.format.anonsubgift": (giftee, months) => `Anónimo le acaba de regalar a ${giftee} una suscripción de ${months} meses`, 49 | "caffeinated.subscription_alert.format.anonresubgift": (giftee, months) => `Anónimo le acaba de regalar a ${giftee} una resuscripción de ${months} meses`, 50 | 51 | // Credits 52 | "caffeinated.credits.title": "Créditos", 53 | 54 | // Settings 55 | "caffeinated.settings.title": "Configuración", 56 | "caffeinated.settings.signout": "Cerrar sesión", 57 | "caffeinated.settings.language": "Idioma", 58 | // TODO "caffeinated.settings.view_changelog": "View Changelog", 59 | // TODO "caffeinated.settings.chatbot_login": "Link chatbot account", 60 | // TODO "caffeinated.settings.chatbot_logout": "Unlink chatbot account", 61 | // TODO "caffeinated.settings.enable_discord_integration": "Enable Discord Integration", 62 | 63 | // Stream Uptime 64 | // TODO "caffeinated.uptime.title": "Stream Uptime", 65 | 66 | // Support Us 67 | "caffeinated.supporters.title": "Apóyennos", 68 | 69 | // Chat Display 70 | "caffeinated.chatdisplay.title": "Chat", 71 | "caffeinated.chatdisplay.join_text": (name) => `${name} se unió a la corriente`, 72 | "caffeinated.chatdisplay.leave_text": (name) => `${name} dejó la corriente`, 73 | "caffeinated.chatdisplay.follow_text": (name) => `${name} te comenzó a seguir`, 74 | "caffeinated.chatdisplay.reward_text": (name, title, image) => `${name} a canjeado ${image}${title}`, 75 | // TODO "caffeinated.chatdisplay.show_viewers": "Show Viewers", 76 | // TODO "caffeinated.chatdisplay.copy_chat_dock_link": "Copy Chat OBS Dock Link", 77 | // TODO "caffeinated.chatdisplay.copy_viewers_dock_link": "Copy Viewers List OBS Dock Link", 78 | 79 | // Chat 80 | "caffeinated.chat.title": "Chat", 81 | "caffeinated.chat.show_donations": "Mostrar Donaciones", 82 | "caffeinated.chat.chat_direction": "Direction", 83 | "caffeinated.chat.chat_animation": "Animación", 84 | "caffeinated.chat.text_align": "Alineación del Texto", 85 | 86 | // Donation Goal 87 | "caffeinated.donation_goal.title": "Objetivo de Donaciónes", 88 | "caffeinated.donation_goal.current_amount": "Monto actual", 89 | 90 | // Follower Goal 91 | "caffeinated.follower_goal.title": "Meta de Seguidores", 92 | 93 | // Generic Goal 94 | "caffeinated.generic_goal.name": "Título", 95 | "caffeinated.generic_goal.goal_amount": "Objetivo", 96 | "caffeinated.generic_goal.text_color": "Color del Título", 97 | "caffeinated.generic_goal.bar_color": "Color de la barra de progreso", 98 | 99 | // Donation Alert 100 | "caffeinated.donation_alert.title": "Alerta de Donación", 101 | "caffeinated.donation_alert.text_to_speech_voice": "Texto a Voz - opcinoes de Voz", 102 | 103 | // Follower Alert 104 | "caffeinated.follower_alert.title": "Alerta de Seguidor", 105 | "caffeinated.follower_alert.format.followed": (user) => `${user} te ha seguido`, 106 | 107 | // Spotify 108 | "spotify.integration.title": "Spotify", 109 | "spotify.integration.login": "Iniciar sesión de Spotify", 110 | "spotify.integration.logging_in": "Iniciando sesión", 111 | "spotify.integration.logged_in_as": (name) => `Conectado como ${name}, (Haga clic para cerrar la sesión)`, 112 | "spotify.integration.announce": "Anunciar Canción", 113 | "spotify.integration.enable_song_command": "Habilitar Comando de Canción", 114 | "spotify.integration.background_style": "Estilo de Fondo", 115 | "spotify.integration.image_style": "Estilo de Imagen", 116 | "spotify.integration.now_playing_announcment": (title, artist) => `Escuchando ${title} - ${artist}`, 117 | 118 | // View Counter 119 | "caffeinated.view_counter.title": "Contador de Espectadores", 120 | 121 | // Recent Follow 122 | "caffeinated.recent_follow.title": "Seguidor Reciente", 123 | 124 | // Donation Ticker 125 | "caffeinated.donation_ticker.title": "Ticker de Donación", 126 | 127 | // Top Donation 128 | "caffeinated.top_donation.title": "Donación Superior", 129 | 130 | // Recent Donation 131 | "caffeinated.recent_donation.title": "Donación Reciente", 132 | 133 | // Recent Subscription 134 | "caffeinated.recent_subscription.title": "Suscripción Reciente", 135 | 136 | // Chat Bot 137 | "caffeinated.chatbot.title": "Chat Bot", 138 | "caffeinated.chatbot.commands": "Comandos", 139 | "caffeinated.chatbot.follow_callout": "Notificación de Seguidor (Dejar en blanco para desactivar)", 140 | "caffeinated.chatbot.donation_callout": "Notificación de Donación (Dejar en blanco para desactivar)", 141 | "caffeinated.chatbot.welcome_callout": "Notificación de Unirse (Dejar en blanco para desactivar)", 142 | "caffeinated.chatbot.default_reply": "Casterlabs es un servicio gratuito de widgets para streaming!", 143 | "caffeinated.chatbot.command_type": "Tipo de Comando", 144 | "caffeinated.chatbot.trigger": "Disparador", 145 | "caffeinated.chatbot.reply": "Responder", 146 | // TODO "caffeinated.chatbot.uptime_command.enable": "Enable Uptime Command", 147 | // TODO "caffeinated.chatbot.uptime_command.format": (time) => `The stream has been up for ${time}`, 148 | // TODO "caffeinated.chatbot.uptime_command.not_live": "We're off the air", 149 | 150 | // Caffeine Integration 151 | "caffeine.integration.title": "Caffeine", 152 | "caffeine.integration.new_thumbnail": "Nueva Miniatura", 153 | "caffeine.integration.rating_selector": "Calificación del contenido", 154 | "caffeine.integration.title_selector": "Título", 155 | "caffeine.integration.game_selector": "Juego Seleccionado", 156 | "caffeine.integration.update": "Actualizar", 157 | 158 | // Casterlabs Companion 159 | "caffeinated.companion.title": "Casterlabs Companion", 160 | "caffeinated.companion.copy": "Copiar el link", 161 | "caffeinated.companion.reset": "Reiniciar el link" 162 | 163 | }, "es-*"); 164 | -------------------------------------------------------------------------------- /app/lang/fr.js: -------------------------------------------------------------------------------- 1 | LANG.absorbLang({ 2 | "meta.language.name": "French", 3 | "meta.language.name.native": "Français", 4 | "meta.language.code": "fr-*", 5 | 6 | // UI 7 | "caffeinated.internal.widgets": "Outils", 8 | "caffeinated.internal.followers_count_text": (count) => `${count} ${(count == 1 ? "follower" : "followers")}`, 9 | "caffeinated.internal.subscribers_count_text": (count) => `${count} ${(count == 1 ? "abonné(e)" : "abonnés")}`, 10 | 11 | // Generic 12 | "generic.enabled": "Activé", 13 | "generic.font": "Police de caractère", 14 | "generic.font.size": "Grandeur de Police de caractère (px)", 15 | "generic.text.color": "Couleur de Texte", 16 | // TODO "generic.background.color": "Background Color", 17 | "generic.volume": "Volume", 18 | "generic.alert.audio": "Audio d'Alerte", 19 | "generic.alert.image": "Image d'Alerte", 20 | "generic.audio.file": "Fichier Audio", 21 | "generic.image.file": "Fichier Image", 22 | "generic.currency": "Devise", 23 | "generic.enable_audio": "Activer l'audio personnalisé", 24 | "generic.use_custom_image": "Utuliser l'image personnalisé", 25 | // TODO "generic.height": "Height (px)", 26 | // TODO "generic.height": "Width (px)", 27 | 28 | // Video Share 29 | // TODO "caffeinated.videoshare.title": "Video Share", 30 | // TODO "caffeinated.videoshare.donations_only": "Donations Only", 31 | // TODO "caffeinated.videoshare.skip": "Skip", 32 | // TODO "caffeinated.videoshare.pause": "Play/Pause", 33 | // TODO "caffeinated.videoshare.player_only": "Player Only (No frame)", 34 | 35 | /* TODO 36 | // Raid 37 | "caffeinated.raid_alert.title": "Raid Alert", 38 | "caffeinated.raid_alert.format.now_raiding": (raider, viewers) => `${raider} just raided with ${viewers} viewers`, 39 | 40 | // Subscription Goal 41 | "caffeinated.subscription_goal.title": "Subscription Goal", 42 | 43 | // Subscription 44 | "caffeinated.subscription_alert.title": "Subscription Alert", 45 | "caffeinated.subscription_alert.format.sub": (name, months, isPlural) => `${name} just subscribed for ${months} month${isPlural ? "s" : ""}`, 46 | "caffeinated.subscription_alert.format.resub": (name, months, isPlural) => `${name} just resubscribed for ${months} month${isPlural ? "s" : ""}`, 47 | "caffeinated.subscription_alert.format.subgift": (name, giftee, months, isPlural) => `${name} just gifted ${giftee} a ${months} month${isPlural ? "s" : ""} subscription`, 48 | "caffeinated.subscription_alert.format.resubgift": (name, giftee, months, isPlural) => `${name} just gifted ${giftee} a ${months} month${isPlural ? "s" : ""} resubscription`, 49 | "caffeinated.subscription_alert.format.anonsubgift": (giftee, months, isPlural) => `Anonymous just gifted ${giftee} a ${months} month${isPlural ? "s" : ""} subscription`, 50 | "caffeinated.subscription_alert.format.anonresubgift": (giftee, months, isPlural) => `Anonymous just gifted ${giftee} a ${months} month${isPlural ? "s" : ""} resubscription`, 51 | */ 52 | 53 | // Credits 54 | "caffeinated.credits.title": "Crédit", 55 | 56 | // Settings 57 | "caffeinated.settings.title": "Réglages", 58 | "caffeinated.settings.signout": "Déconnexion", 59 | "caffeinated.settings.language": "Langue", 60 | // TODO "caffeinated.settings.view_changelog": "View Changelog", 61 | // TODO "caffeinated.settings.chatbot_login": "Link chatbot account", 62 | // TODO "caffeinated.settings.chatbot_logout": "Unlink chatbot account", 63 | // TODO "caffeinated.settings.enable_discord_integration": "Enable Discord Integration", 64 | 65 | // Stream Uptime 66 | // TODO "caffeinated.uptime.title": "Stream Uptime", 67 | 68 | // Support Us 69 | "caffeinated.supporters.title": "Nous Soutenir", 70 | 71 | // Chat Display 72 | "caffeinated.chatdisplay.title": "Chat", 73 | "caffeinated.chatdisplay.join_text": (name) => `${name} a rejoint le flux vidéo`, 74 | "caffeinated.chatdisplay.leave_text": (name) => `${name} quitter le flux vidéo`, 75 | "caffeinated.chatdisplay.follow_text": (name) => `${name} a commencé à suivre`, 76 | // TODO "caffeinated.chatdisplay.reward_text": (name, title, image) => `${name} just redeemed ${image}${title}`, 77 | // TODO "caffeinated.chatdisplay.show_viewers": "Show Viewers", 78 | // TODO "caffeinated.chatdisplay.copy_chat_dock_link": "Copy Chat OBS Dock Link", 79 | // TODO "caffeinated.chatdisplay.copy_viewers_dock_link": "Copy Viewers List OBS Dock Link", 80 | 81 | // Chat 82 | "caffeinated.chat.title": "Chat", 83 | "caffeinated.chat.show_donations": "Montre Donations", 84 | "caffeinated.chat.chat_direction": "Direction", 85 | "caffeinated.chat.chat_animation": "Animation", 86 | "caffeinated.chat.text_align": "Alignment de Texte", 87 | 88 | // Donation Goal 89 | "caffeinated.donation_goal.title": "Objectif de don", 90 | "caffeinated.donation_goal.current_amount": "Montant actuel", 91 | 92 | // Follower Goal 93 | "caffeinated.follower_goal.title": "Objectif suiveur", 94 | 95 | // Generic Goal 96 | "caffeinated.generic_goal.name": "Titre", 97 | "caffeinated.generic_goal.goal_amount": "Montant cible", 98 | "caffeinated.generic_goal.text_color": "Couleur de Titre", 99 | "caffeinated.generic_goal.bar_color": "Couleur de la barre", 100 | 101 | // Donation Alert 102 | "caffeinated.donation_alert.title": "Alerte de don", 103 | "caffeinated.donation_alert.text_to_speech_voice": "Voix de synthèse vocale", 104 | 105 | // Follower Alert 106 | "caffeinated.follower_alert.title": "Alerte Suiveur", 107 | // TODO "caffeinated.follower_alert.format.followed": (user) => `${user} just followed`, 108 | 109 | // Spotify 110 | "spotify.integration.title": "Spotify", 111 | "spotify.integration.login": "Connectez-vous avec Spotify", 112 | "spotify.integration.logging_in": "Se connecter", 113 | "spotify.integration.logged_in_as": (name) => `Connecté en tant que ${name}, (Cliquez pour vous déconnecter)`, 114 | "spotify.integration.announce": "Annoncer la chanson", 115 | "spotify.integration.enable_song_command": "Activer la commande de chanson", 116 | "spotify.integration.background_style": "Style de fond", 117 | "spotify.integration.image_style": "Style d'Image", 118 | "spotify.integration.now_playing_announcment": (title, artist) => `Lecture en cours: ${title} - ${artist}`, 119 | 120 | // View Counter 121 | "caffeinated.view_counter.title": "Compteur de Vue", 122 | 123 | // Recent Follow 124 | "caffeinated.recent_follow.title": "Suivi récent", 125 | 126 | // Donation Ticker 127 | "caffeinated.donation_ticker.title": "Compteur de dons", 128 | 129 | // Top Donation 130 | "caffeinated.top_donation.title": "Don le Plus Grand", 131 | 132 | // Recent Donation 133 | "caffeinated.recent_donation.title": "Don Recent", 134 | 135 | // Recent Subscription 136 | "caffeinated.recent_subscription.title": "Subscription Recent", 137 | 138 | // Chat Bot 139 | "caffeinated.chatbot.title": "Robot de Chat", 140 | "caffeinated.chatbot.commands": "Commandes", 141 | "caffeinated.chatbot.follow_callout": "Notification Suiveur (Laisser vide pour désactiver)", 142 | "caffeinated.chatbot.donation_callout": "Notification de Don (Laisser vide pour désactiver)", 143 | "caffeinated.chatbot.default_reply": "Casterlabs est un service gratuit pour des modules de vidéo en streaming!", 144 | "caffeinated.chatbot.command_type": "Type de Commande", 145 | "caffeinated.chatbot.trigger": "Déclencheur", 146 | "caffeinated.chatbot.reply": "Repond", 147 | // TODO "caffeinated.chatbot.uptime_command.enable": "Enable Uptime Command", 148 | // TODO "caffeinated.chatbot.uptime_command.format": (time) => `The stream has been up for ${time}`, 149 | // TODO "caffeinated.chatbot.uptime_command.not_live": "We're off the air", 150 | 151 | // Caffeine Integration 152 | "caffeine.integration.title": "Caffeine", 153 | "caffeine.integration.new_thumbnail": "Nouvelle miniature", 154 | "caffeine.integration.rating_selector": "Évaluation du contenu", 155 | "caffeine.integration.title_selector": "Titre", 156 | "caffeine.integration.game_selector": "Jeux selectionner", 157 | "caffeine.integration.update": "Mettre à jour", 158 | 159 | // Casterlabs Companion 160 | "caffeinated.companion.title": "Companion Casterlabs", 161 | "caffeinated.companion.copy": "Copier le lien", 162 | "caffeinated.companion.reset": "Réinitialiser le lien" 163 | 164 | }, "fr-*"); 165 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require("electron"); 2 | 3 | const createWindow = require("./createWindow.js"); 4 | 5 | // Disable web cache. 6 | app.commandLine.appendSwitch("disable-http-cache"); 7 | 8 | // This method will be called when Electron has finished 9 | // initialization and is ready to create browser windows. 10 | // Some APIs can only be used after this event occurs. 11 | app.whenReady().then(() => { 12 | createWindow(__dirname); 13 | }); 14 | 15 | // Quit when all windows are closed. 16 | app.on("window-all-closed", function () { 17 | // On macOS it is common for applications and their menu bar 18 | // to stay active until the user quits explicitly with Cmd + Q 19 | if (process.platform !== "darwin") { 20 | app.quit(); 21 | } 22 | }) 23 | 24 | app.on("activate", function () { 25 | // On macOS it's common to re-create a window in the app when the 26 | // dock icon is clicked and there are no other windows open. 27 | if (BrowserWindow.getAllWindows().length === 0) { 28 | createWindow(); 29 | } 30 | }) 31 | 32 | app.disableHardwareAcceleration(); -------------------------------------------------------------------------------- /app/media/app_icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thehelvijs/Caffeinated/016add60642b88667decd23411605911f865e2f8/app/media/app_icon.icns -------------------------------------------------------------------------------- /app/media/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thehelvijs/Caffeinated/016add60642b88667decd23411605911f865e2f8/app/media/app_icon.ico -------------------------------------------------------------------------------- /app/media/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thehelvijs/Caffeinated/016add60642b88667decd23411605911f865e2f8/app/media/app_icon.png -------------------------------------------------------------------------------- /app/media/caffeinated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thehelvijs/Caffeinated/016add60642b88667decd23411605911f865e2f8/app/media/caffeinated.png -------------------------------------------------------------------------------- /app/media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thehelvijs/Caffeinated/016add60642b88667decd23411605911f865e2f8/app/media/icon.png -------------------------------------------------------------------------------- /app/modules/brime/brime.js: -------------------------------------------------------------------------------- 1 | const BRIME_UNCATEGORIZED_ID = "606e93525fa50e5780970135"; 2 | 3 | function searchForBrimeCategory(query) { 4 | return new Promise((resolve, reject) => { 5 | fetch(`https://api-staging.brimelive.com/internal/category/search?client_id=${BRIME_CLIENT_ID}&q=${encodeURIComponent(query)}`) 6 | .then((response) => response.json()) 7 | .then((response) => { 8 | let categories = {}; 9 | 10 | for (const category of response.data.result) { 11 | categories[category.name] = category._id; 12 | } 13 | 14 | resolve(categories); 15 | }) 16 | .catch(reject) 17 | }); 18 | } 19 | 20 | MODULES.uniqueModuleClasses["brime_integration"] = class { 21 | 22 | constructor(id) { 23 | this.namespace = "brime_integration"; 24 | this.displayname = "Brime"; 25 | this.type = "settings"; 26 | this.id = id; 27 | this.supportedPlatforms = ["BRIME"]; 28 | this.persist = true; 29 | 30 | this.queryingCategories = false; 31 | 32 | this.categories = {}; 33 | } 34 | 35 | async updateSearchContents() { 36 | if (this.queryingCategories) { 37 | setTimeout(() => { 38 | if (!this.queryingCategories) { 39 | this.updateSearchContents(); 40 | } 41 | }, 500); 42 | } else { 43 | this.queryingCategories = true; 44 | 45 | try { 46 | const query = this.page.querySelector("[name='category'] .sns-input").value; 47 | this.categories = await searchForBrimeCategory(query); 48 | 49 | const categorySearchElement = this.page.querySelector("[name='category']"); 50 | 51 | SELECTNSEARCH.populate(categorySearchElement, Object.keys(this.categories)); 52 | } finally { 53 | this.queryingCategories = false; 54 | } 55 | } 56 | } 57 | 58 | async sendUpdate() { 59 | const { authorization, client_id } = await koi.getCredentials(); 60 | 61 | const headers = new Headers({ 62 | authorization: authorization, 63 | client_id: client_id, 64 | "content-type": "application/json" 65 | }); 66 | 67 | const title = this.page.querySelector("[name='title']").value; 68 | const vodsEnabled = this.page.querySelector("[name='vods_enabled']").checked; 69 | const category = this.categories[this.page.querySelector("[name='category'] .sns-input").value] ?? BRIME_UNCATEGORIZED_ID; 70 | 71 | fetch(`https://api-staging.brimelive.com/v1/channel/me`, { 72 | headers: headers 73 | }) 74 | .then((response) => response.json()) 75 | .then((channelData) => { 76 | // TEMP 77 | const { description } = channelData.data; 78 | 79 | fetch("https://api-staging.brimelive.com/internal/channel/update", { 80 | headers: headers, 81 | method: "POST", 82 | body: JSON.stringify({ 83 | description: description, 84 | vodsEnabled: vodsEnabled, 85 | title: title, 86 | category: category 87 | }) 88 | }); 89 | 90 | }) 91 | } 92 | 93 | getDataToStore() { 94 | return this.settings; 95 | } 96 | 97 | init() { 98 | this.page 99 | .querySelector("[name='category'] .sns-input") 100 | .addEventListener("keydown", () => { 101 | this.updateSearchContents(); 102 | }) 103 | } 104 | 105 | onSettingsUpdate() { 106 | this.updated = true; 107 | } 108 | 109 | settingsDisplay = { 110 | title: { 111 | display: "Stream Title", 112 | type: "input", 113 | isLang: false 114 | }, 115 | category: { 116 | display: "Category", 117 | type: "search", 118 | isLang: false 119 | }, 120 | vods_enabled: { 121 | display: "Store past broadcasts", 122 | type: "checkbox", 123 | isLang: false 124 | }, 125 | update: { 126 | display: "Update", 127 | type: "button", 128 | isLang: false 129 | } 130 | }; 131 | 132 | defaultSettings = { 133 | title: "LIVE on Brime!", 134 | category: ["Uncategorized"], 135 | vods_enabled: true, 136 | update: () => this.sendUpdate() 137 | }; 138 | 139 | }; 140 | -------------------------------------------------------------------------------- /app/modules/caffeine/caffeine.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.uniqueModuleClasses["caffeine_integration"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "caffeine_integration"; 6 | this.displayname = "caffeine.integration.title"; 7 | this.type = "settings"; 8 | this.id = id; 9 | this.supportedPlatforms = ["CAFFEINE"]; 10 | this.persist = true; 11 | 12 | this.schedule = -1; 13 | 14 | const RATINGS = { 15 | "Mature": "M", 16 | "PG": "PG", 17 | "Unrated": "UNRATED" 18 | }; 19 | 20 | const GAMES = { 21 | "Entertainment": 79 // Unsure how to go about this, so I just set it to OBS 22 | }; 23 | 24 | this.defaultSettings.game.push("Entertainment"); 25 | 26 | { 27 | const xhr = new XMLHttpRequest(); 28 | 29 | xhr.open("GET", "https://api.caffeine.tv/v1/games", false); 30 | xhr.send(null); 31 | 32 | JSON.parse(xhr.responseText).forEach((game) => { 33 | GAMES[game.name] = game.id; 34 | this.defaultSettings.game.push(game.name); 35 | }); 36 | } 37 | 38 | this.defaultSettings.update = () => { 39 | const form = new FormData(); 40 | 41 | // Have to do this for the client to display it as 17+ 42 | const title = (this.settings.rating === "Mature") ? ("[17+] " + this.settings.title) : this.settings.title; 43 | const rating = RATINGS[this.settings.rating]; 44 | 45 | form.append("broadcast[game_id]", GAMES[this.settings.game]); 46 | form.append("broadcast[name]", title); 47 | form.append("broadcast[content_rating]", rating); 48 | 49 | if (this.settings.new_thumbnail.files && (this.settings.new_thumbnail.files.length > 0)) { 50 | form.append("broadcast[game_image]", this.settings.new_thumbnail.files[0]); 51 | } 52 | 53 | this.getBroadcastId().then((broadcastId) => { 54 | if (this.schedule != -1) { 55 | clearInterval(this.schedule); 56 | this.schedule = -1; 57 | } 58 | 59 | const task = () => { 60 | koi.getCredentials().then((credentials) => { 61 | fetch("https://api.caffeine.tv/v1/broadcasts/" + broadcastId, { 62 | method: "PATCH", 63 | headers: { 64 | "Authorization": credentials.authorization, 65 | "x-client-type": "web" // I see. 66 | }, 67 | body: form 68 | }); 69 | }).catch(() => { 70 | clearInterval(this.schedule); 71 | }); 72 | }; 73 | 74 | this.schedule = setInterval(task, (5 * 60) * 1000); // Every 5 minutes 75 | 76 | task(); 77 | }); 78 | }; 79 | } 80 | 81 | getBroadcastId() { 82 | return new Promise((resolve) => { 83 | fetch("https://api.caffeine.tv/v1/users/" + CAFFEINATED.userdata.streamer.UUID) 84 | .then((response) => response.json()) 85 | .then((profileData) => { 86 | resolve(profileData.user.broadcast_id); 87 | }); 88 | }); 89 | } 90 | 91 | getDataToStore() { 92 | return this.settings; 93 | } 94 | 95 | onSettingsUpdate() { 96 | this.updated = true; 97 | } 98 | 99 | settingsDisplay = { 100 | game: { 101 | display: "caffeine.integration.game_selector", 102 | type: "search", 103 | isLang: true 104 | }, 105 | rating: { 106 | display: "caffeine.integration.rating_selector", 107 | type: "select", 108 | isLang: true 109 | }, 110 | title: { 111 | display: "caffeine.integration.title_selector", 112 | type: "input", 113 | isLang: true 114 | }, 115 | new_thumbnail: { 116 | display: "caffeine.integration.new_thumbnail", 117 | type: "file", 118 | isLang: true 119 | }, 120 | update: { 121 | display: "caffeine.integration.update", 122 | type: "button", 123 | isLang: true 124 | } 125 | }; 126 | 127 | defaultSettings = { 128 | game: [], 129 | rating: [ 130 | "Mature", 131 | "PG", 132 | "Unrated" 133 | ], 134 | title: "LIVE on Caffeine!", 135 | // update: function() { } 136 | }; 137 | 138 | }; 139 | -------------------------------------------------------------------------------- /app/modules/ko-fi/kofimodule.js: -------------------------------------------------------------------------------- 1 | let KOFI_ENABLED = false; 2 | 3 | MODULES.uniqueModuleClasses["kofi_integration"] = class { 4 | 5 | constructor(id) { 6 | this.namespace = "kofi_integration"; 7 | this.displayname = "Ko-fi Integration"; 8 | this.type = "settings"; 9 | this.id = id; 10 | this.persist = true; 11 | 12 | this.kinoko = new Kinoko(); 13 | 14 | this.defaultSettings.url = () => { 15 | putInClipboard(`https://api.casterlabs.co/v1/kinoko?channel=${encodeURIComponent(this.uuid)}`); 16 | } 17 | 18 | this.kinoko.on("close", () => this.kinoko.connect(this.uuid, "parent")); 19 | 20 | this.kinoko.on("message", (form) => { 21 | const data = JSON.parse(form.data); 22 | 23 | console.debug(data); 24 | 25 | if (data.is_public) { 26 | const isTest = data.kofi_transaction_id === "1234-1234-1234-1234"; 27 | const id = isTest ? data.kofi_transaction_id : ""; 28 | 29 | if (data.is_subscription_payment) { 30 | koi.broadcast("subscription", { 31 | emotes: {}, 32 | mensions: [], 33 | links: [], 34 | streamer: CAFFEINATED.userdata.streamer, 35 | subscriber: { 36 | platform: "KOFI", 37 | image_link: "https://ko-fi.com/favicon.png", 38 | followers_count: -1, 39 | badges: [], 40 | color: "#00B9FE", 41 | username: data.from_name.toLowerCase(), 42 | displayname: data.from_name, 43 | UUID: data.from_name, 44 | UPID: `${data.from_name};KOFI`, 45 | link: data.url 46 | }, 47 | months: 1, 48 | sub_type: data.is_first_subscription_payment ? "SUB" : "RESUB", 49 | sub_level: "TIER_1", 50 | message: "", 51 | id: id, 52 | upvotable: false, 53 | event_type: "SUBSCRIPTION" 54 | }); 55 | } else { 56 | const currency = isTest ? data.currency : "USD"; 57 | const message = isTest ? data.message : "Test from Ko-fi"; 58 | const amount = isTest ? 0 : parseFloat(data.amount); 59 | 60 | koi.broadcast("donation", { 61 | donations: [ 62 | { 63 | "animated_image": "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", 64 | "currency": currency, 65 | "amount": amount, 66 | "image": "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", 67 | "type": "KOFI_DIRECT", 68 | "name": "Ko-fi Donation" 69 | } 70 | ], 71 | emotes: {}, 72 | mensions: [], 73 | links: [], 74 | streamer: CAFFEINATED.userdata.streamer, 75 | sender: { 76 | platform: "KOFI", 77 | image_link: "https://ko-fi.com/favicon.png", 78 | followers_count: -1, 79 | badges: [], 80 | color: "#00B9FE", 81 | username: data.from_name.toLowerCase(), 82 | displayname: data.from_name, 83 | UUID: data.from_name, 84 | UPID: `${data.from_name};KOFI`, 85 | link: data.url 86 | }, 87 | message: message, 88 | id: id, 89 | upvotable: false, 90 | event_type: "DONATION" 91 | }); 92 | } 93 | } 94 | }); 95 | 96 | } 97 | 98 | getDataToStore() { 99 | return { 100 | uuid: this.uuid, 101 | enabled: this.settings.enabled 102 | }; 103 | } 104 | 105 | onSettingsUpdate() { 106 | KOFI_ENABLED = this.settings.enabled; 107 | koi.broadcast("kofi_update", { enabled: this.settings.enabled }); 108 | } 109 | 110 | init() { 111 | this.uuid = this.settings.uuid; 112 | 113 | if (!this.uuid || this.uuid.includes("-")) { 114 | this.uuid = `kofi_signaling:${generateUnsafeUniquePassword(64)}`; 115 | MODULES.saveToStore(this); 116 | } 117 | 118 | KOFI_ENABLED = this.settings.enabled; 119 | 120 | koi.broadcast("kofi_update", { enabled: this.settings.enabled }); 121 | 122 | this.kinoko.connect(this.uuid, "parent"); 123 | 124 | const instructions = document.createElement("div"); 125 | 126 | instructions.innerHTML = ` 127 | Go here, then paste the provided url into the Webhook URL field and hit Update. 128 |
129 |
130 | You can test to see if this worked by hitting Send Test. 131 | `; 132 | 133 | this.page.firstChild.appendChild(instructions); 134 | } 135 | 136 | settingsDisplay = { 137 | enabled: "checkbox", 138 | url: { 139 | display: "Copy Webhook URL", 140 | type: "button" 141 | } 142 | }; 143 | 144 | defaultSettings = { 145 | enabled: false 146 | // url: () => {} 147 | }; 148 | 149 | }; 150 | -------------------------------------------------------------------------------- /app/modules/modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "supported": [ 3 | "1.*-*" 4 | ], 5 | "unsupported": [ 6 | "0.4.*-*", 7 | "0.5.*-*" 8 | ], 9 | "version": "1.0.0", 10 | "name": "Caffeinated Default Widgets Repo", 11 | "scripts": [ 12 | "modules/donation.js", 13 | "modules/follower.js", 14 | "modules/chat.js", 15 | "modules/followergoal.js", 16 | "modules/donationgoal.js", 17 | "modules/supporters.js", 18 | "modules/chatdisplay.js", 19 | "modules/companion.js", 20 | "modules/rain.js", 21 | "modules/nowplaying.js", 22 | "modules/viewcounter.js", 23 | "modules/recentfollow.js", 24 | "modules/recentdonation.js", 25 | "modules/topdonation.js", 26 | "modules/followcounter.js", 27 | "modules/donationticker.js", 28 | "modules/bot.js", 29 | "modules/raidalert.js", 30 | "modules/subscriptionalert.js", 31 | "modules/subscriptiongoal.js", 32 | "modules/recentsubscription.js", 33 | "modules/subscribercounter.js", 34 | "modules/videoshare.js", 35 | "modules/uptime.js", 36 | "caffeine/caffeine.js", 37 | "brime/brime.js", 38 | "ko-fi/kofimodule.js" 39 | ], 40 | "simple": [ 41 | { 42 | "namespace": "casterlabs_chat", 43 | "id": "chat" 44 | }, 45 | { 46 | "namespace": "casterlabs_donation", 47 | "id": "donation" 48 | }, 49 | { 50 | "namespace": "casterlabs_donation_goal", 51 | "id": "donation_goal" 52 | }, 53 | { 54 | "namespace": "casterlabs_recent_donation", 55 | "id": "recent_donation" 56 | }, 57 | { 58 | "namespace": "casterlabs_top_donation", 59 | "id": "top_donation" 60 | }, 61 | { 62 | "namespace": "casterlabs_donation_ticker", 63 | "id": "donation_ticker" 64 | }, 65 | { 66 | "namespace": "casterlabs_rain", 67 | "id": "emoji_rain" 68 | }, 69 | { 70 | "namespace": "casterlabs_follower", 71 | "id": "follower" 72 | }, 73 | { 74 | "namespace": "casterlabs_follower_goal", 75 | "id": "follower_goal" 76 | }, 77 | { 78 | "namespace": "casterlabs_recent_follow", 79 | "id": "recent_follow" 80 | }, 81 | { 82 | "namespace": "casterlabs_follow_counter", 83 | "id": "follow_counter" 84 | }, 85 | { 86 | "namespace": "casterlabs_raid", 87 | "id": "raid" 88 | }, 89 | { 90 | "namespace": "casterlabs_subscription", 91 | "id": "subscription" 92 | }, 93 | { 94 | "namespace": "casterlabs_subscription_goal", 95 | "id": "subscription_goal" 96 | }, 97 | { 98 | "namespace": "casterlabs_recent_subscription", 99 | "id": "recent_subscription" 100 | }, 101 | { 102 | "namespace": "casterlabs_subscriber_counter", 103 | "id": "subscriber_counter" 104 | }, 105 | { 106 | "namespace": "casterlabs_now_playing", 107 | "id": "now_playing" 108 | }, 109 | { 110 | "namespace": "casterlabs_view_counter", 111 | "id": "viewer_counter" 112 | }, 113 | { 114 | "namespace": "casterlabs_uptime", 115 | "id": "uptime" 116 | } 117 | ], 118 | "required": [ 119 | { 120 | "namespace": "caffeine_integration", 121 | "id": "caffeine_integration" 122 | }, 123 | { 124 | "namespace": "brime_integration", 125 | "id": "brime_integration" 126 | }, 127 | { 128 | "namespace": "casterlabs_bot", 129 | "id": "chat_bot" 130 | }, 131 | { 132 | "namespace": "kofi_integration", 133 | "id": "ko-fi_integration" 134 | }, 135 | { 136 | "namespace": "casterlabs_companion", 137 | "id": "casterlabs_companion" 138 | }, 139 | { 140 | "namespace": "casterlabs_supporters", 141 | "id": "casterlabs_supporters" 142 | }, 143 | { 144 | "namespace": "casterlabs_chat_display", 145 | "id": "chat_display" 146 | }, 147 | { 148 | "namespace": "casterlabs_video_share", 149 | "id": "video_share" 150 | } 151 | ] 152 | } -------------------------------------------------------------------------------- /app/modules/modules/bot.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.uniqueModuleClasses["casterlabs_bot"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_bot"; 6 | this.displayname = "caffeinated.chatbot.title"; 7 | this.type = "settings"; 8 | this.id = id; 9 | this.persist = true; 10 | } 11 | 12 | getDataToStore() { 13 | return this.settings; 14 | } 15 | 16 | init() { 17 | this.limitFields(); 18 | 19 | koi.addEventListener("user_update", () => { 20 | setTimeout(() => this.limitFields(), 100); 21 | }); 22 | 23 | koi.addEventListener("chat", (event) => { 24 | if (this.settings.enabled) { 25 | this.processCommand(event); 26 | } 27 | }); 28 | 29 | koi.addEventListener("donation", (event) => { 30 | if (this.settings.enabled) { 31 | if (this.settings.donation_callout) { 32 | koi.sendMessage(`@${event.sender.displayname} ${this.settings.donation_callout}`, event, "PUPPET"); 33 | } 34 | 35 | this.processCommand(event); 36 | } 37 | }); 38 | 39 | koi.addEventListener("viewer_join", (event) => { 40 | if (this.settings.enabled) { 41 | if (this.settings.welcome_callout && (event.streamer.platform === "TROVO")) { 42 | koi.sendMessage(`@${event.sender.displayname} ${this.settings.welcome_callout}`, event, "PUPPET"); 43 | } 44 | } 45 | }); 46 | 47 | koi.addEventListener("follow", (event) => { 48 | if (this.settings.enabled) { 49 | if (this.settings.follow_callout) { 50 | koi.sendMessage(`@${event.follower.displayname} ${this.settings.follow_callout}`, event, "PUPPET"); 51 | } 52 | } 53 | }); 54 | } 55 | 56 | processCommand(event) { 57 | const message = event.message.toLowerCase(); 58 | 59 | if (this.settings.enable_uptime_command && message.startsWith("!uptime")) { 60 | if (CAFFEINATED.streamdata && CAFFEINATED.streamdata.is_live) { 61 | const millis = CAFFEINATED.getTimeLiveInMilliseconds(); 62 | const formatted = getFriendlyTime(millis); 63 | 64 | koi.sendMessage(`@${event.sender.displayname} ${LANG.getTranslation("caffeinated.chatbot.uptime_command.format", formatted)} `, event, "PUPPET"); 65 | } else { 66 | koi.sendMessage(`@${event.sender.displayname} ${LANG.getTranslation("caffeinated.chatbot.uptime_command.not_live")} `, event, "PUPPET"); 67 | } 68 | return; 69 | } 70 | 71 | for (const command of this.settings.commands) { 72 | if (message.endsWith(command.reply.toLowerCase())) { 73 | return; // Loop detected. 74 | } 75 | } 76 | 77 | // Second pass. 78 | for (const command of this.settings.commands) { 79 | const trigger = command.trigger.toLowerCase(); 80 | 81 | if ((command.type == "Script") && message.startsWith(trigger)) { 82 | const eventVar = "const event = arguments[0];\n"; 83 | const result = looseInterpret(eventVar + command.reply, event); 84 | 85 | if (result) { 86 | if (result instanceof Promise) { 87 | result.then((message) => { 88 | if (message) { 89 | koi.sendMessage(message.toString(), event, "PUPPET"); 90 | } 91 | }) 92 | } else { 93 | koi.sendMessage(result.toString(), event, "PUPPET"); 94 | } 95 | } 96 | 97 | return; 98 | } else if ( 99 | ((command.type == "Command") && message.startsWith(trigger)) || 100 | ((command.type == "Keyword") && message.includes(trigger)) 101 | ) { 102 | koi.sendMessage(`@${event.sender.displayname} ${command.reply}`, event, "PUPPET"); 103 | return; 104 | } 105 | } 106 | } 107 | 108 | limitFields() { 109 | // It's 10 less to help fit in the mention 110 | const max = koi.getMaxLength() - 10; 111 | 112 | /* 113 | Array.from(this.page.querySelectorAll("[name=reply][owner=chat_bot]")).forEach((element) => { 114 | element.setAttribute("maxlength", max); 115 | }); 116 | */ 117 | 118 | this.page.querySelector("[name=follow_callout][owner=chat_bot]").setAttribute("maxlength", max); 119 | this.page.querySelector("[name=donation_callout][owner=chat_bot]").setAttribute("maxlength", max); 120 | this.page.querySelector("[name=welcome_callout][owner=chat_bot]").setAttribute("maxlength", max); 121 | 122 | if (CAFFEINATED.userdata && (CAFFEINATED.userdata.streamer.platform === "TROVO")) { 123 | this.page.querySelector("[name=welcome_callout][owner=chat_bot]").parentElement.classList.remove("hide"); 124 | } else { 125 | this.page.querySelector("[name=welcome_callout][owner=chat_bot]").parentElement.classList.add("hide"); 126 | } 127 | } 128 | 129 | onSettingsUpdate() { 130 | this.limitFields(); 131 | } 132 | 133 | settingsDisplay = { 134 | enabled: { 135 | display: "generic.enabled", 136 | type: "checkbox", 137 | isLang: true 138 | }, 139 | commands: { 140 | display: "caffeinated.chatbot.commands", 141 | type: "dynamic", 142 | isLang: true 143 | }, 144 | enable_uptime_command: { 145 | display: "caffeinated.chatbot.uptime_command.enable", 146 | type: "checkbox", 147 | isLang: true 148 | }, 149 | follow_callout: { 150 | display: "caffeinated.chatbot.follow_callout", 151 | type: "input", 152 | isLang: true 153 | }, 154 | donation_callout: { 155 | display: "caffeinated.chatbot.donation_callout", 156 | type: "input", 157 | isLang: true 158 | }, 159 | welcome_callout: { 160 | display: "caffeinated.chatbot.welcome_callout", 161 | type: "input", 162 | isLang: true 163 | } 164 | }; 165 | 166 | defaultSettings = { 167 | enabled: false, 168 | commands: { 169 | display: { 170 | type: { 171 | display: "caffeinated.chatbot.command_type", 172 | type: "select", 173 | isLang: true 174 | }, 175 | trigger: { 176 | display: "caffeinated.chatbot.trigger", 177 | type: "input", 178 | isLang: true 179 | }, 180 | reply: { 181 | display: "caffeinated.chatbot.reply", 182 | type: "textarea", 183 | isLang: true 184 | } 185 | }, 186 | default: { 187 | type: ["Command", "Keyword", "Script"], 188 | trigger: "!casterlabs", 189 | mention: true, 190 | reply: LANG.getTranslation("caffeinated.chatbot.default_reply") 191 | } 192 | }, 193 | enable_uptime_command: true, 194 | follow_callout: "", 195 | donation_callout: "", 196 | welcome_callout: "" 197 | }; 198 | 199 | }; 200 | -------------------------------------------------------------------------------- /app/modules/modules/chat.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_chat"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_chat"; 6 | this.displayname = "caffeinated.chat.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | } 10 | 11 | widgetDisplay = [ 12 | { 13 | name: "Test", 14 | icon: "dice", 15 | onclick(instance) { 16 | koi.test("chat"); 17 | } 18 | }, 19 | { 20 | name: "Copy", 21 | icon: "copy", 22 | onclick(instance) { 23 | putInClipboard("https://caffeinated.casterlabs.co/chat.html?id=" + instance.id); 24 | } 25 | } 26 | ] 27 | 28 | getDataToStore() { 29 | return this.settings; 30 | } 31 | 32 | onConnection(socket) { 33 | MODULES.emitIO(this, "config", this.settings, socket); 34 | } 35 | 36 | init() { 37 | const instance = this; 38 | 39 | koi.addEventListener("meta", (event) => { 40 | MODULES.emitIO(instance, "event", event); 41 | }); 42 | 43 | koi.addEventListener("chat", (event) => { 44 | MODULES.emitIO(instance, "event", event); 45 | }); 46 | 47 | koi.addEventListener("donation", (event) => { 48 | if (instance.settings.show_donations) { 49 | MODULES.emitIO(instance, "event", event); 50 | } 51 | }); 52 | 53 | koi.addEventListener("channel_points", (event) => { 54 | // this.addPointStatus(event.sender, event.reward); 55 | }); 56 | 57 | koi.addEventListener("follow", (event) => { 58 | // this.addStatus(event.follower, "caffeinated.chatdisplay.follow_text"); 59 | }); 60 | 61 | } 62 | 63 | // TODO status messages. 64 | /* 65 | addStatus(profile, langKey) { 66 | const usernameHtml = `${escapeHtml(profile.displayname)}`; 67 | const lang = LANG.getTranslation(langKey, usernameHtml); 68 | 69 | this.addManualStatus(lang); 70 | } 71 | 72 | addPointStatus(profile, reward) { 73 | const usernameHtml = ` ${escapeHtml(profile.displayname)} `; 74 | const imageHtml = ` `; 75 | 76 | const lang = LANG.getTranslation("caffeinated.chatdisplay.reward_text", usernameHtml, reward.title, imageHtml); 77 | 78 | this.addManualStatus(lang); 79 | } 80 | 81 | addManualStatus(lang) { 82 | MODULES.emitIO(instance, "event", { 83 | type: "STATUS", 84 | lang: lang 85 | }); 86 | } 87 | */ 88 | 89 | onSettingsUpdate() { 90 | MODULES.emitIO(this, "config", this.settings); 91 | } 92 | 93 | settingsDisplay = { 94 | font: { 95 | display: "generic.font", 96 | type: "font", 97 | isLang: true 98 | }, 99 | font_size: { 100 | display: "generic.font.size", 101 | type: "number", 102 | isLang: true 103 | }, 104 | text_color: { 105 | display: "generic.text.color", 106 | type: "color", 107 | isLang: true 108 | }, 109 | show_donations: { 110 | display: "caffeinated.chat.show_donations", 111 | type: "checkbox", 112 | isLang: true 113 | }, 114 | chat_direction: { 115 | display: "caffeinated.chat.chat_direction", 116 | type: "select", 117 | isLang: true 118 | }, 119 | chat_animation: { 120 | display: "caffeinated.chat.chat_animation", 121 | type: "select", 122 | isLang: true 123 | }, 124 | text_align: { 125 | display: "caffeinated.chat.text_align", 126 | type: "select", 127 | isLang: true 128 | } 129 | }; 130 | 131 | defaultSettings = { 132 | font: "Poppins", 133 | font_size: "16", 134 | show_donations: true, 135 | text_color: "#FFFFFF", 136 | chat_direction: [ 137 | "Down", 138 | "Up" 139 | ], 140 | chat_animation: [ 141 | "Default", 142 | "Slide", 143 | "Slide (Disappearing)", 144 | "Disappearing" 145 | ], 146 | text_align: [ 147 | "Left", 148 | "Right" 149 | ] 150 | }; 151 | 152 | }; 153 | -------------------------------------------------------------------------------- /app/modules/modules/companion.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.uniqueModuleClasses["casterlabs_companion"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_companion"; 6 | this.displayname = "caffeinated.companion.title"; 7 | this.type = "settings"; 8 | this.id = id; 9 | this.persist = true; 10 | 11 | this.messageHistory = []; 12 | this.viewersList = []; 13 | 14 | this.kinoko = new Kinoko(); 15 | 16 | this.defaultSettings.reset_link = () => { 17 | this.uuid = generateUnsafeUniquePassword(16); 18 | this.setLinkText(); 19 | this.connect(); 20 | MODULES.saveToStore(this); 21 | }; 22 | 23 | this.defaultSettings.copy_link = () => { 24 | putInClipboard(`https://casterlabs.co/companion?key=${this.uuid}`); 25 | }; 26 | 27 | this.kinoko.on("message", (data) => { 28 | switch (data.type.toLowerCase()) { 29 | case "connected": { 30 | this.sendAll(); 31 | return; 32 | } 33 | 34 | } 35 | }); 36 | 37 | } 38 | 39 | sendAll() { 40 | if (CAFFEINATED.userdata) { 41 | this.sendEvent("message_history", this.messageHistory, true); 42 | this.sendEvent("viewers_list", this.viewersList, true); 43 | } 44 | } 45 | 46 | sendEvent(type, event, isCatchup = false) { 47 | this.send("event", { 48 | type: type, 49 | event: event, 50 | is_catchup: isCatchup 51 | }); 52 | } 53 | 54 | send(type, data) { 55 | this.kinoko.send({ 56 | type: type, 57 | data: data 58 | }); 59 | } 60 | 61 | getDataToStore() { 62 | return { 63 | uuid: this.uuid, 64 | enabled: this.settings.enabled 65 | }; 66 | } 67 | 68 | connect() { 69 | this.kinoko.disconnect(); 70 | 71 | if (this.settings.enabled) { 72 | this.kinoko.connect("companion:" + this.uuid, "parent"); 73 | } 74 | } 75 | 76 | setLinkText() { 77 | this.qrWindow.setCode(`https://casterlabs.co/companion?key=${this.uuid}`); 78 | } 79 | 80 | init() { 81 | this.qrWindow = this.page.querySelector("iframe").contentWindow; 82 | this.uuid = this.settings.uuid; 83 | 84 | if (!this.uuid || this.uuid.includes("-")) { 85 | this.uuid = generateUnsafeUniquePassword(16); 86 | 87 | MODULES.saveToStore(this); 88 | } 89 | 90 | this.page.querySelector("iframe").style.marginBottom = "35px"; 91 | 92 | // Give the frame time to render. 93 | setTimeout(() => { 94 | this.setLinkText(); 95 | this.connect(); 96 | }, 5000); 97 | 98 | koi.addEventListener("chat", (event) => { 99 | this.addMessage(event); 100 | }); 101 | 102 | koi.addEventListener("donation", (event) => { 103 | this.addMessage(event); 104 | }); 105 | 106 | koi.addEventListener("meta", (event) => { 107 | this.messageMeta(event); 108 | }); 109 | 110 | koi.addEventListener("channel_points", (event) => { 111 | this.addPointStatus(event.sender, event.reward, "caffeinated.chatdisplay.reward_text", event.id); 112 | }); 113 | 114 | koi.addEventListener("follow", (event) => { 115 | this.addStatus(event.follower, "caffeinated.chatdisplay.follow_text"); 116 | }); 117 | 118 | koi.addEventListener("subscription", (event) => { 119 | const profile = event.gift_recipient ?? event.subscriber; 120 | 121 | this.addManualStatus(profile, LANG.formatSubscription(event)); 122 | }); 123 | 124 | koi.addEventListener("viewer_join", (event) => { 125 | this.addStatus(event.viewer, "caffeinated.chatdisplay.join_text"); 126 | }); 127 | 128 | koi.addEventListener("viewer_leave", (event) => { 129 | this.addStatus(event.viewer, "caffeinated.chatdisplay.leave_text"); 130 | }); 131 | 132 | koi.addEventListener("viewer_list", (event) => { 133 | this.viewersList = event.viewers; 134 | 135 | this.sendEvent("viewers_list", this.viewersList); 136 | }); 137 | 138 | } 139 | 140 | /* Handler Code */ 141 | messageMeta(event) { 142 | this.messageHistory.push({ 143 | type: "META", 144 | event: Object.assign({}, event) 145 | }); 146 | 147 | this.sendEvent("meta", event); 148 | } 149 | 150 | addMessage(event) { 151 | this.messageHistory.push({ 152 | type: "MESSAGE", 153 | event: Object.assign({}, event) 154 | }); 155 | 156 | this.sendEvent("message", event); 157 | } 158 | 159 | addStatus(profile, langKey, id) { 160 | const usernameHtml = `${escapeHtml(profile.displayname)}`; 161 | const lang = LANG.getTranslation(langKey, usernameHtml); 162 | 163 | const event = { 164 | profile: profile, 165 | lang: lang, 166 | id: id 167 | }; 168 | 169 | this.messageHistory.push({ 170 | type: "STATUS", 171 | event: event 172 | }); 173 | 174 | this.sendEvent("status", event); 175 | } 176 | 177 | addPointStatus(profile, reward, langKey) { 178 | const usernameHtml = ` ${escapeHtml(profile.displayname)} `; 179 | const imageHtml = ` `; 180 | 181 | const lang = LANG.getTranslation(langKey, usernameHtml, reward.title, imageHtml); 182 | 183 | const event = { 184 | profile: profile, 185 | lang: lang, id: id 186 | }; 187 | 188 | this.messageHistory.push({ 189 | type: "STATUS", 190 | event: event 191 | }); 192 | 193 | this.sendEvent("status", event); 194 | } 195 | 196 | addManualStatus(profile, status) { 197 | const event = { 198 | profile: profile, 199 | lang: status 200 | }; 201 | 202 | this.messageHistory.push({ 203 | type: "STATUS", 204 | event: event 205 | }); 206 | 207 | this.sendEvent("status", event); 208 | } 209 | 210 | onSettingsUpdate() { 211 | this.setLinkText(); 212 | this.connect(); 213 | } 214 | 215 | settingsDisplay = { 216 | enabled: { 217 | display: "generic.enabled", 218 | type: "checkbox", 219 | isLang: true 220 | }, 221 | qr_frame: { 222 | type: "iframe-src", 223 | height: "175px", 224 | width: "175px" 225 | }, 226 | copy_link: { 227 | display: "caffeinated.companion.copy", 228 | type: "button", 229 | isLang: true 230 | }, 231 | reset_link: { 232 | display: "caffeinated.companion.reset", 233 | type: "button", 234 | isLang: true 235 | } 236 | }; 237 | 238 | defaultSettings = { 239 | enabled: false, 240 | qr_frame: __dirname + "/modules/modules/qr.html", 241 | // copy_link: () => {}, 242 | // reset_link: () => {} 243 | }; 244 | 245 | }; 246 | -------------------------------------------------------------------------------- /app/modules/modules/donation.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | MODULES.moduleClasses["casterlabs_donation"] = class { 4 | 5 | constructor(id) { 6 | this.namespace = "casterlabs_donation"; 7 | this.displayname = "caffeinated.donation_alert.title"; 8 | this.type = "overlay settings"; 9 | this.id = id; 10 | this.supportedPlatforms = ["TWITCH", "CAFFEINE", "TROVO"]; 11 | } 12 | 13 | widgetDisplay = [ 14 | { 15 | name: "Test", 16 | icon: "dice", 17 | onclick(instance) { 18 | koi.test("donation"); 19 | } 20 | }, 21 | { 22 | name: "Copy", 23 | icon: "copy", 24 | onclick(instance) { 25 | putInClipboard("https://caffeinated.casterlabs.co/donation.html?id=" + instance.id); 26 | } 27 | } 28 | ] 29 | 30 | getDataToStore() { 31 | FileStore.setFile(this, "audio_file", this.audio_file); 32 | FileStore.setFile(this, "image_file", this.image_file); 33 | 34 | return nullFields(this.settings, ["audio_file", "image_file"]); 35 | } 36 | 37 | onConnection(socket) { 38 | MODULES.emitIO(this, "config", this.settings, socket); 39 | MODULES.emitIO(this, "audio_file", this.audio_file, socket); 40 | MODULES.emitIO(this, "image_file", this.image_file, socket); 41 | } 42 | 43 | init() { 44 | koi.addEventListener("donation", (event) => { 45 | for (const donation of event.donations) { 46 | const converted = Object.assign({ 47 | image: donation.image, 48 | animated_image: donation.animated_image 49 | }, event); 50 | 51 | MODULES.emitIO(this, "event", converted); 52 | 53 | // Only alert once for Caffeine props 54 | if (event.sender.platform == "CAFFEINE") { 55 | return; 56 | } 57 | } 58 | }); 59 | 60 | if (this.settings.audio_file) { 61 | this.audio_file = this.settings.audio_file; 62 | delete this.settings.audio_file; 63 | 64 | MODULES.saveToStore(this); 65 | } else { 66 | this.audio_file = FileStore.getFile(this, "audio_file", this.audio_file); 67 | } 68 | 69 | if (this.settings.image_file) { 70 | this.image_file = this.settings.image_file; 71 | delete this.settings.image_file; 72 | 73 | MODULES.saveToStore(this); 74 | } else { 75 | this.image_file = FileStore.getFile(this, "image_file", this.image_file); 76 | } 77 | } 78 | 79 | async onSettingsUpdate() { 80 | MODULES.emitIO(this, "config", nullFields(this.settings, ["audio_file", "image_file"])); 81 | 82 | if (this.settings.audio_file.files.length > 0) { 83 | this.audio_file = await fileToBase64(this.settings.audio_file, "audio"); 84 | 85 | MODULES.emitIO(this, "audio_file", this.audio_file); 86 | } 87 | 88 | if (this.settings.image_file.files.length > 0) { 89 | this.image_file = await fileToBase64(this.settings.image_file); 90 | 91 | MODULES.emitIO(this, "image_file", this.image_file); 92 | } 93 | } 94 | 95 | settingsDisplay = { 96 | font: { 97 | display: "generic.font", 98 | type: "font", 99 | isLang: true 100 | }, 101 | font_size: { 102 | display: "generic.font.size", 103 | type: "number", 104 | isLang: true 105 | }, 106 | text_color: { 107 | display: "generic.text.color", 108 | type: "color", 109 | isLang: true 110 | }, 111 | volume: { 112 | display: "generic.volume", 113 | type: "range", 114 | isLang: true 115 | }, 116 | text_to_speech_voice: { 117 | display: "caffeinated.donation_alert.text_to_speech_voice", 118 | type: "select", 119 | isLang: true 120 | }, 121 | audio: { 122 | display: "generic.alert.audio", 123 | type: "select", 124 | isLang: true 125 | }, 126 | image: { 127 | display: "generic.alert.image", 128 | type: "select", 129 | isLang: true 130 | }, 131 | audio_file: { 132 | display: "generic.audio.file", 133 | type: "file", 134 | isLang: true 135 | }, 136 | image_file: { 137 | display: "generic.image.file", 138 | type: "file", 139 | isLang: true 140 | } 141 | }; 142 | 143 | defaultSettings = { 144 | font: "Poppins", 145 | font_size: "16", 146 | text_color: "#FFFFFF", 147 | volume: 1, 148 | text_to_speech_voice: ["Brian", "Russell", "Nicole", "Amy", "Salli", "Joanna", "Matthew", "Ivy", "Joey"], 149 | audio: ["Custom Audio", "Text To Speech", "Custom Audio & Text To Speech", "None"], 150 | image: ["Custom Image", "Animated Donation Image", "Donation Image", "None"], 151 | audio_file: "", 152 | image_file: "" 153 | }; 154 | 155 | }; 156 | -------------------------------------------------------------------------------- /app/modules/modules/donationgoal.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_donation_goal"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_donation_goal"; 6 | this.displayname = "caffeinated.donation_goal.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | this.supportedPlatforms = ["TWITCH", "CAFFEINE", "TROVO"]; 10 | } 11 | 12 | widgetDisplay = [ 13 | { 14 | name: "Reset", 15 | icon: "trash", 16 | async onclick(instance) { 17 | instance.amount = 0; 18 | 19 | instance.sendUpdates(); 20 | MODULES.saveToStore(instance); 21 | } 22 | }, 23 | { 24 | name: "Copy", 25 | icon: "copy", 26 | onclick(instance) { 27 | putInClipboard("https://caffeinated.casterlabs.co/goal.html?namespace=" + instance.namespace + "&id=" + instance.id); 28 | } 29 | } 30 | ] 31 | 32 | getDataToStore() { 33 | const data = Object.assign({}, this.settings); 34 | 35 | data.amount = this.amount; 36 | 37 | return data; 38 | } 39 | 40 | async onConnection(socket) { 41 | this.sendUpdates(socket); 42 | } 43 | 44 | init() { 45 | this.amount = this.settings.amount; 46 | 47 | if (!this.amount) this.amount = 0; 48 | 49 | koi.addEventListener("donation", async (event) => { 50 | if (!event.isTest) { 51 | for (const donation of event.donations) { 52 | this.amount += (await convertCurrency(donation.amount, donation.currency, "USD")); 53 | } 54 | 55 | this.sendUpdates(); 56 | MODULES.saveToStore(this); 57 | } 58 | }); 59 | } 60 | 61 | async onSettingsUpdate() { 62 | const current = parseFloat(this.page.querySelector("[name=current_amount]").value); 63 | 64 | if (this.oldAmount != current) { 65 | this.amount = (await convertCurrency(current, this.settings.currency, "USD")); 66 | } 67 | 68 | this.sendUpdates(); 69 | } 70 | 71 | async sendUpdates(socket) { 72 | MODULES.emitIO(this, "config", this.settings, socket); 73 | 74 | this.oldAmount = this.amount; 75 | 76 | const convertedAmount = (await convertCurrency(this.amount, "USD", this.settings.currency)); 77 | 78 | this.page.querySelector("[name=current_amount]").value = convertedAmount; 79 | 80 | MODULES.emitIO(this, "amount", convertedAmount, socket); 81 | MODULES.emitIO(this, "display", (await convertAndFormatCurrency(this.amount, "USD", this.settings.currency)), socket); 82 | MODULES.emitIO(this, "goaldisplay", formatCurrency(this.settings.goal_amount, this.settings.currency), socket); 83 | } 84 | 85 | settingsDisplay = { 86 | title: { 87 | display: "caffeinated.generic_goal.name", 88 | type: "input", 89 | isLang: true 90 | }, 91 | currency: { 92 | display: "generic.currency", 93 | type: "currency", 94 | isLang: true 95 | }, 96 | current_amount: { 97 | display: "caffeinated.donation_goal.current_amount", 98 | type: "number", 99 | isLang: true 100 | }, 101 | goal_amount: { 102 | display: "caffeinated.generic_goal.goal_amount", 103 | type: "number", 104 | isLang: true 105 | }, 106 | height: { 107 | display: "generic.height", 108 | type: "number", 109 | isLang: true 110 | }, 111 | font: { 112 | display: "generic.font", 113 | type: "font", 114 | isLang: true 115 | }, 116 | font_size: { 117 | display: "generic.font.size", 118 | type: "number", 119 | isLang: true 120 | }, 121 | text_color: { 122 | display: "caffeinated.generic_goal.text_color", 123 | type: "color", 124 | isLang: true 125 | }, 126 | bar_color: { 127 | display: "caffeinated.generic_goal.bar_color", 128 | type: "color", 129 | isLang: true 130 | } 131 | }; 132 | 133 | defaultSettings = { 134 | title: "", 135 | currency: "USD", 136 | current_amount: 10, 137 | goal_amount: 10, 138 | height: 60, 139 | font: "Roboto", 140 | font_size: 28, 141 | text_color: "#FFFFFF", 142 | bar_color: "#31F8FF" 143 | }; 144 | 145 | }; 146 | -------------------------------------------------------------------------------- /app/modules/modules/donationticker.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_donation_ticker"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_donation_ticker"; 6 | this.displayname = "caffeinated.donation_ticker.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | this.supportedPlatforms = ["TWITCH", "CAFFEINE", "TROVO"]; 10 | } 11 | 12 | widgetDisplay = [ 13 | { 14 | name: "Copy", 15 | icon: "copy", 16 | onclick(instance) { 17 | putInClipboard("https://caffeinated.casterlabs.co/display.html?namespace=" + instance.namespace + "&id=" + instance.id); 18 | } 19 | } 20 | ] 21 | 22 | getDataToStore() { 23 | const data = Object.assign({}, this.settings); 24 | 25 | data.amount = this.amount; 26 | 27 | return data; 28 | } 29 | 30 | onConnection(socket) { 31 | this.update(socket); 32 | } 33 | 34 | init() { 35 | this.amount = this.settings.amount; 36 | 37 | if (this.amount === undefined) { 38 | this.amount = 0; 39 | } 40 | 41 | koi.addEventListener("donation", async (event) => { 42 | if (!event.isTest) { 43 | for (const donation of event.donations) { 44 | this.amount += (await convertCurrency(donation.amount, donation.currency, "USD")); 45 | } 46 | 47 | MODULES.saveToStore(this); 48 | this.update(); 49 | } 50 | }); 51 | } 52 | 53 | async onSettingsUpdate() { 54 | this.update(); 55 | } 56 | 57 | async update(socket) { 58 | MODULES.emitIO(this, "config", this.settings, socket); 59 | 60 | const amount = await convertAndFormatCurrency(this.amount, "USD", this.settings.currency); 61 | 62 | MODULES.emitIO(this, "event", ` 63 | 64 | ${amount} 65 | 66 | `, socket); 67 | } 68 | 69 | settingsDisplay = { 70 | font: { 71 | display: "generic.font", 72 | type: "font", 73 | isLang: true 74 | }, 75 | font_size: { 76 | display: "generic.font.size", 77 | type: "number", 78 | isLang: true 79 | }, 80 | currency: { 81 | display: "generic.currency", 82 | type: "currency", 83 | isLang: true 84 | }, 85 | text_color: { 86 | display: "generic.text.color", 87 | type: "color", 88 | isLang: true 89 | } 90 | }; 91 | 92 | defaultSettings = { 93 | font: "Poppins", 94 | currency: "USD", 95 | font_size: 24, 96 | text_color: "#FFFFFF" 97 | }; 98 | 99 | }; 100 | -------------------------------------------------------------------------------- /app/modules/modules/followcounter.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_follow_counter"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_follow_counter"; 6 | this.displayname = "caffeinated.follow_counter.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | 10 | } 11 | 12 | widgetDisplay = [ 13 | { 14 | name: "Copy", 15 | icon: "copy", 16 | onclick(instance) { 17 | putInClipboard("https://caffeinated.casterlabs.co/display.html?namespace=" + instance.namespace + "&id=" + instance.id); 18 | } 19 | } 20 | ] 21 | 22 | getDataToStore() { 23 | return this.settings; 24 | } 25 | 26 | onConnection(socket) { 27 | this.update(socket); 28 | } 29 | 30 | init() { 31 | koi.addEventListener("user_data", (event) => { 32 | this.update(null, event); 33 | }); 34 | } 35 | 36 | async onSettingsUpdate() { 37 | this.update(); 38 | } 39 | 40 | update(socket, userdata = CAFFEINATED.userdata) { 41 | MODULES.emitIO(this, "config", this.settings, socket); 42 | 43 | if (userdata) { 44 | MODULES.emitIO(this, "event", ` 45 | 46 | ${userdata.streamer.followers_count} 47 | 48 | `, socket); 49 | } else { 50 | MODULES.emitIO(this, "event", "", socket); 51 | } 52 | } 53 | 54 | settingsDisplay = { 55 | font: { 56 | display: "generic.font", 57 | type: "font", 58 | isLang: true 59 | }, 60 | font_size: { 61 | display: "generic.font.size", 62 | type: "number", 63 | isLang: true 64 | }, 65 | text_color: { 66 | display: "generic.text.color", 67 | type: "color", 68 | isLang: true 69 | } 70 | }; 71 | 72 | defaultSettings = { 73 | font: "Poppins", 74 | font_size: 24, 75 | text_color: "#FFFFFF" 76 | }; 77 | 78 | }; 79 | -------------------------------------------------------------------------------- /app/modules/modules/follower.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_follower"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_follower"; 6 | this.displayname = "caffeinated.follower_alert.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | } 10 | 11 | widgetDisplay = [ 12 | { 13 | name: "Test", 14 | icon: "dice", 15 | onclick(instance) { 16 | koi.test("follow"); 17 | } 18 | }, 19 | { 20 | name: "Copy", 21 | icon: "copy", 22 | onclick(instance) { 23 | putInClipboard("https://caffeinated.casterlabs.co/alert.html?namespace=" + instance.namespace + "&id=" + instance.id); 24 | } 25 | } 26 | ] 27 | 28 | getDataToStore() { 29 | FileStore.setFile(this, "audio_file", this.audio_file); 30 | FileStore.setFile(this, "image_file", this.image_file); 31 | 32 | return nullFields(this.settings, ["audio_file", "image_file"]); 33 | } 34 | 35 | onConnection(socket) { 36 | MODULES.emitIO(this, "config", this.settings, socket); 37 | MODULES.emitIO(this, "audio_file", this.audio_file, socket); 38 | MODULES.emitIO(this, "image_file", this.image_file, socket); 39 | 40 | } 41 | 42 | init() { 43 | koi.addEventListener("follow", (event) => { 44 | const follower = `${event.follower.displayname}`; 45 | 46 | MODULES.emitIO(this, "event", LANG.getTranslation("caffeinated.follower_alert.format.followed", follower)); 47 | }); 48 | 49 | if (this.settings.audio_file) { 50 | this.audio_file = this.settings.audio_file; 51 | delete this.settings.audio_file; 52 | 53 | MODULES.saveToStore(this); 54 | } else { 55 | this.audio_file = FileStore.getFile(this, "audio_file", this.audio_file); 56 | } 57 | 58 | if (this.settings.image_file) { 59 | this.image_file = this.settings.image_file; 60 | delete this.settings.image_file; 61 | 62 | MODULES.saveToStore(this); 63 | } else { 64 | this.image_file = FileStore.getFile(this, "image_file", this.image_file); 65 | } 66 | } 67 | 68 | async onSettingsUpdate() { 69 | MODULES.emitIO(this, "config", nullFields(this.settings, ["audio_file", "image_file"])); 70 | 71 | if (this.settings.audio_file.files.length > 0) { 72 | this.audio_file = await fileToBase64(this.settings.audio_file, "audio"); 73 | 74 | MODULES.emitIO(this, "audio_file", this.audio_file); 75 | } 76 | 77 | if (this.settings.image_file.files.length > 0) { 78 | this.image_file = await fileToBase64(this.settings.image_file); 79 | 80 | MODULES.emitIO(this, "image_file", this.image_file); 81 | } 82 | } 83 | 84 | settingsDisplay = { 85 | font: { 86 | display: "generic.font", 87 | type: "font", 88 | isLang: true 89 | }, 90 | font_size: { 91 | display: "generic.font.size", 92 | type: "number", 93 | isLang: true 94 | }, 95 | text_color: { 96 | display: "generic.text.color", 97 | type: "color", 98 | isLang: true 99 | }, 100 | volume: { 101 | display: "generic.volume", 102 | type: "range", 103 | isLang: true 104 | }, 105 | enable_audio: { 106 | display: "generic.enable_audio", 107 | type: "checkbox", 108 | isLang: true 109 | }, 110 | use_custom_image: { 111 | display: "generic.use_custom_image", 112 | type: "checkbox", 113 | isLang: true 114 | }, 115 | audio_file: { 116 | display: "generic.audio.file", 117 | type: "file", 118 | isLang: true 119 | }, 120 | image_file: { 121 | display: "generic.image.file", 122 | type: "file", 123 | isLang: true 124 | } 125 | }; 126 | 127 | defaultSettings = { 128 | font: "Poppins", 129 | font_size: "16", 130 | text_color: "#FFFFFF", 131 | volume: 1, 132 | enable_audio: false, 133 | use_custom_image: false, 134 | audio_file: "", 135 | image_file: "" 136 | }; 137 | 138 | }; 139 | -------------------------------------------------------------------------------- /app/modules/modules/followergoal.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_follower_goal"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_follower_goal"; 6 | this.displayname = "caffeinated.follower_goal.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | } 10 | 11 | widgetDisplay = [ 12 | { 13 | name: "Reset", 14 | icon: "trash", 15 | async onclick(instance) { 16 | instance.amount = 0; 17 | 18 | instance.sendUpdates(); 19 | MODULES.saveToStore(instance); 20 | } 21 | }, 22 | { 23 | name: "Copy", 24 | icon: "copy", 25 | onclick(instance) { 26 | putInClipboard("https://caffeinated.casterlabs.co/goal.html?namespace=" + instance.namespace + "&id=" + instance.id); 27 | } 28 | } 29 | ] 30 | 31 | getDataToStore() { 32 | const data = Object.assign({}, this.settings); 33 | 34 | data.amount = this.amount; 35 | 36 | return data; 37 | } 38 | 39 | async onConnection(socket) { 40 | this.sendUpdates(socket); 41 | } 42 | 43 | init() { 44 | this.amount = this.settings.amount; 45 | 46 | if (!this.amount) this.amount = 0; 47 | 48 | koi.addEventListener("user_update", (event) => { 49 | this.amount = event.streamer.followers_count; 50 | 51 | this.sendUpdates(); 52 | MODULES.saveToStore(this); 53 | }); 54 | } 55 | 56 | onSettingsUpdate() { 57 | this.sendUpdates(); 58 | } 59 | 60 | async sendUpdates(socket) { 61 | MODULES.emitIO(this, "config", this.settings, socket); 62 | 63 | MODULES.emitIO(this, "amount", this.amount, socket); 64 | MODULES.emitIO(this, "display", this.amount, socket); 65 | MODULES.emitIO(this, "goaldisplay", this.settings.goal_amount, socket); 66 | } 67 | 68 | settingsDisplay = { 69 | title: { 70 | display: "caffeinated.generic_goal.name", 71 | type: "input", 72 | isLang: true 73 | }, 74 | goal_amount: { 75 | display: "caffeinated.generic_goal.goal_amount", 76 | type: "number", 77 | isLang: true 78 | }, 79 | height: { 80 | display: "generic.height", 81 | type: "number", 82 | isLang: true 83 | }, 84 | font: { 85 | display: "generic.font", 86 | type: "font", 87 | isLang: true 88 | }, 89 | font_size: { 90 | display: "generic.font.size", 91 | type: "number", 92 | isLang: true 93 | }, 94 | text_color: { 95 | display: "caffeinated.generic_goal.text_color", 96 | type: "color", 97 | isLang: true 98 | }, 99 | bar_color: { 100 | display: "caffeinated.generic_goal.bar_color", 101 | type: "color", 102 | isLang: true 103 | } 104 | }; 105 | 106 | defaultSettings = { 107 | title: "", 108 | goal_amount: 10, 109 | height: 60, 110 | font: "Roboto", 111 | font_size: 28, 112 | text_color: "#FFFFFF", 113 | bar_color: "#31F8FF" 114 | }; 115 | 116 | }; 117 | -------------------------------------------------------------------------------- /app/modules/modules/nowplaying.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_now_playing"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_now_playing"; 6 | this.displayname = "spotify.integration.title" 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | } 10 | 11 | widgetDisplay = [ 12 | { 13 | name: "Copy", 14 | icon: "copy", 15 | onclick(instance) { 16 | putInClipboard("https://caffeinated.casterlabs.co/nowplaying.html?id=" + instance.id); 17 | } 18 | } 19 | ] 20 | 21 | async setToken(code) { 22 | const response = await fetch("https://api.casterlabs.co/v2/natsukashii/spotify?code=" + code); 23 | const authResult = await response.json(); 24 | 25 | if (!authResult.error) { 26 | this.statusElement.innerText = LANG.getTranslation("spotify.integration.logging_in"); 27 | 28 | this.refreshToken = authResult.refresh_token; 29 | 30 | MODULES.saveToStore(this); 31 | } else { 32 | this.settings.token = null; 33 | this.statusElement.innerText = LANG.getTranslation("spotify.integration.login"); 34 | } 35 | } 36 | 37 | getDataToStore() { 38 | const data = Object.assign({}, this.settings); 39 | 40 | data.token = this.refreshToken; 41 | 42 | return data; 43 | } 44 | 45 | onConnection(socket) { 46 | MODULES.emitIO(this, "config", this.settings, socket); 47 | 48 | if (this.event) { 49 | MODULES.emitIO(this, "event", this.event, socket); 50 | } 51 | } 52 | 53 | init() { 54 | if (this.settings.token) { 55 | this.refreshToken = this.settings.token; 56 | this.settings.token = null; 57 | this.check(); 58 | } 59 | 60 | koi.addEventListener("chat", (event) => { 61 | this.processCommand(event); 62 | }); 63 | 64 | koi.addEventListener("donation", (event) => { 65 | this.processCommand(event); 66 | }); 67 | 68 | setInterval(() => this.check(), 2000); 69 | 70 | const element = document.querySelector("#casterlabs_now_playing_" + this.id).querySelector("[name=login]"); 71 | 72 | element.style = "overflow: hidden; background-color: rgb(30, 215, 96); margin-top: 15px;"; 73 | element.innerHTML = ` 74 | 75 | ${LANG.getTranslation("spotify.integration.login")} 76 | `; 77 | 78 | this.statusElement = element.querySelector("[name=text]"); 79 | } 80 | 81 | processCommand(event) { 82 | const message = event.message.toLowerCase(); 83 | 84 | if (message.startsWith("!song")) { 85 | if (this.settings.enable_song_command) { 86 | koi.sendMessage(`@${event.sender.displayname} ${this.event.title} - ${this.event.artist}`, event, "PUPPET"); 87 | } 88 | } 89 | } 90 | 91 | async check() { 92 | if (this.refreshToken) { 93 | if (!this.accessToken) { 94 | const auth = await (await fetch("https://api.casterlabs.co/v2/natsukashii/spotify?refresh_token=" + this.refreshToken)).json(); 95 | 96 | if (auth.error) { 97 | this.refreshToken = null; 98 | this.statusElement.innerText = LANG.getTranslation("spotify.integration.login"); 99 | } else { 100 | this.statusElement.innerText = LANG.getTranslation("spotify.integration.logging_in"); 101 | 102 | this.accessToken = auth.access_token; 103 | if (auth.refresh_token) { 104 | this.refreshToken = auth.refresh_token; 105 | } 106 | 107 | const profile = await (await fetch("https://api.spotify.com/v1/me", { 108 | headers: { 109 | "content-type": "application/json", 110 | authorization: "Bearer " + this.accessToken 111 | } 112 | })).json(); 113 | 114 | this.statusElement.innerText = LANG.getTranslation("spotify.integration.logged_in_as", profile.display_name); 115 | } 116 | 117 | MODULES.saveToStore(this); 118 | } 119 | 120 | const response = await fetch("https://api.spotify.com/v1/me/player", { 121 | headers: { 122 | "content-type": "application/json", 123 | authorization: "Bearer " + this.accessToken 124 | } 125 | }); 126 | 127 | if ((response.status == 401) || response.error) { 128 | this.accessToken = null; 129 | this.check(); 130 | } else if (response.status == 200) { 131 | const player = await response.json(); 132 | 133 | if (player.item) { 134 | const image = player.item.album.images[0].url; 135 | const title = player.item.name.replace(/(\(ft.*\))|(\(feat.*\))/gi, ""); // Remove (feat. ...) 136 | let artists = []; 137 | 138 | player.item.artists.forEach((artist) => { 139 | artists.push(artist.name); 140 | }); 141 | 142 | this.broadcast({ 143 | title: title, 144 | artist: artists.join(", "), 145 | image: image 146 | }); 147 | } 148 | } 149 | } 150 | } 151 | 152 | broadcast(event) { 153 | if (this.event) { 154 | // Don't re-notify 155 | if (this.event.title == event.title) { 156 | return; 157 | } 158 | } 159 | 160 | this.event = event; 161 | 162 | if (this.settings.announce) { 163 | koi.sendMessage(`Now playing: ${event.title} - ${event.artist}`, CAFFEINATED.userdata, "PUPPET"); 164 | } 165 | 166 | MODULES.emitIO(this, "event", this.event); 167 | } 168 | 169 | onSettingsUpdate() { 170 | MODULES.emitIO(this, "config", this.settings); 171 | } 172 | 173 | settingsDisplay = { 174 | login: { 175 | display: "spotify.integration.login", 176 | type: "button", 177 | isLang: true 178 | }, 179 | announce: { 180 | display: "spotify.integration.announce", 181 | type: "checkbox", 182 | isLang: true 183 | }, 184 | enable_song_command: { 185 | display: "spotify.integration.enable_song_command", 186 | type: "checkbox", 187 | isLang: true 188 | }, 189 | background_style: { 190 | display: "spotify.integration.background_style", 191 | type: "select", 192 | isLang: true 193 | }, 194 | image_style: { 195 | display: "spotify.integration.image_style", 196 | type: "select", 197 | isLang: true 198 | } 199 | }; 200 | 201 | defaultSettings = { 202 | login: () => { 203 | if (this.refreshToken) { 204 | this.refreshToken = null; 205 | this.accessToken = null; 206 | this.statusElement.innerText = LANG.getTranslation("spotify.integration.login"); 207 | } else { 208 | const auth = new AuthCallback("caffeinated_spotify"); 209 | 210 | // 15min timeout 211 | auth.awaitAuthMessage((15 * 1000) * 60).then((token) => { 212 | this.setToken(token); 213 | }).catch((reason) => { /* Ignored. */ }); 214 | 215 | openLink("https://accounts.spotify.com/en/authorize?client_id=dff9da1136b0453983ff40e3e5e20397&response_type=code&scope=user-read-playback-state&redirect_uri=https:%2F%2Fcasterlabs.co%2Fauth%3Ftype%3Dcaffeinated_spotify&state=" + auth.getStateString()); 216 | } 217 | }, 218 | announce: false, 219 | enable_song_command: false, 220 | background_style: ["Blur", "Clear", "Solid"], 221 | image_style: ["Left", "Right", "None"] 222 | }; 223 | 224 | }; 225 | -------------------------------------------------------------------------------- /app/modules/modules/qr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Caffeinated QR Code 6 | 7 | 8 | 53 | 54 | 55 | 56 | 57 |
58 | 59 | Click to unhide 60 |
61 | QR code 62 |
63 |
64 | 65 | 66 | 99 | 100 | -------------------------------------------------------------------------------- /app/modules/modules/raidalert.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_raid"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_raid"; 6 | this.displayname = "caffeinated.raid_alert.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | this.supportedPlatforms = ["TWITCH", "TROVO", "BRIME"]; 10 | } 11 | 12 | widgetDisplay = [ 13 | { 14 | name: "Test", 15 | icon: "dice", 16 | onclick(instance) { 17 | koi.test("raid"); 18 | } 19 | }, 20 | { 21 | name: "Copy", 22 | icon: "copy", 23 | onclick(instance) { 24 | putInClipboard("https://caffeinated.casterlabs.co/alert.html?namespace=" + instance.namespace + "&id=" + instance.id); 25 | } 26 | } 27 | ] 28 | 29 | getDataToStore() { 30 | FileStore.setFile(this, "audio_file", this.audio_file); 31 | FileStore.setFile(this, "image_file", this.image_file); 32 | 33 | return nullFields(this.settings, ["audio_file", "image_file"]); 34 | } 35 | 36 | onConnection(socket) { 37 | MODULES.emitIO(this, "config", this.settings, socket); 38 | MODULES.emitIO(this, "audio_file", this.audio_file, socket); 39 | MODULES.emitIO(this, "image_file", this.image_file, socket); 40 | 41 | } 42 | 43 | init() { 44 | koi.addEventListener("raid", (event) => { 45 | MODULES.emitIO( 46 | this, 47 | "event", 48 | `${event.host.displayname} just raided with ${event.viewers} ${event.viewers == 1 ? "viewer" : "viewers"}` 49 | ); 50 | }); 51 | 52 | if (this.settings.audio_file) { 53 | this.audio_file = this.settings.audio_file; 54 | delete this.settings.audio_file; 55 | 56 | MODULES.saveToStore(this); 57 | } else { 58 | this.audio_file = FileStore.getFile(this, "audio_file", this.audio_file); 59 | } 60 | 61 | if (this.settings.image_file) { 62 | this.image_file = this.settings.image_file; 63 | delete this.settings.image_file; 64 | 65 | MODULES.saveToStore(this); 66 | } else { 67 | this.image_file = FileStore.getFile(this, "image_file", this.image_file); 68 | } 69 | } 70 | 71 | async onSettingsUpdate() { 72 | MODULES.emitIO(this, "config", nullFields(this.settings, ["audio_file", "image_file"])); 73 | 74 | if (this.settings.audio_file.files.length > 0) { 75 | this.audio_file = await fileToBase64(this.settings.audio_file, "audio"); 76 | 77 | MODULES.emitIO(this, "audio_file", this.audio_file); 78 | } 79 | 80 | if (this.settings.image_file.files.length > 0) { 81 | this.image_file = await fileToBase64(this.settings.image_file); 82 | 83 | MODULES.emitIO(this, "image_file", this.image_file); 84 | } 85 | } 86 | 87 | settingsDisplay = { 88 | font: { 89 | display: "generic.font", 90 | type: "font", 91 | isLang: true 92 | }, 93 | font_size: { 94 | display: "generic.font.size", 95 | type: "number", 96 | isLang: true 97 | }, 98 | text_color: { 99 | display: "generic.text.color", 100 | type: "color", 101 | isLang: true 102 | }, 103 | volume: { 104 | display: "generic.volume", 105 | type: "range", 106 | isLang: true 107 | }, 108 | enable_audio: { 109 | display: "generic.enable_audio", 110 | type: "checkbox", 111 | isLang: true 112 | }, 113 | use_custom_image: { 114 | display: "generic.use_custom_image", 115 | type: "checkbox", 116 | isLang: true 117 | }, 118 | audio_file: { 119 | display: "generic.audio.file", 120 | type: "file", 121 | isLang: true 122 | }, 123 | image_file: { 124 | display: "generic.image.file", 125 | type: "file", 126 | isLang: true 127 | } 128 | }; 129 | 130 | defaultSettings = { 131 | font: "Poppins", 132 | font_size: "16", 133 | text_color: "#FFFFFF", 134 | volume: 1, 135 | enable_audio: true, 136 | use_custom_image: true, 137 | audio_file: "", 138 | image_file: "" 139 | }; 140 | 141 | }; 142 | -------------------------------------------------------------------------------- /app/modules/modules/rain.js: -------------------------------------------------------------------------------- 1 | 2 | // TODO lang 3 | MODULES.moduleClasses["casterlabs_rain"] = class { 4 | 5 | constructor(id) { 6 | this.namespace = "casterlabs_rain"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | } 10 | 11 | widgetDisplay = [ 12 | { 13 | name: "Test", 14 | icon: "dice", 15 | onclick(instance) { 16 | MODULES.emitIO(instance, "event", { 17 | message: "🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉" 18 | }); 19 | } 20 | }, 21 | { 22 | name: "Copy", 23 | icon: "copy", 24 | onclick(instance) { 25 | putInClipboard("https://caffeinated.casterlabs.co/emoji.html?id=" + instance.id); 26 | } 27 | } 28 | ] 29 | 30 | getDataToStore() { 31 | return this.settings; 32 | } 33 | 34 | onConnection(socket) { 35 | MODULES.emitIO(this, "config", this.settings, socket); 36 | } 37 | 38 | init() { 39 | const instance = this; 40 | 41 | koi.addEventListener("chat", (event) => { 42 | this.addCustomEmotes(event); 43 | MODULES.emitIO(instance, "event", event); 44 | }); 45 | 46 | koi.addEventListener("donation", (event) => { 47 | this.addCustomEmotes(event); 48 | MODULES.emitIO(instance, "event", event); 49 | }); 50 | } 51 | 52 | onSettingsUpdate() { 53 | MODULES.emitIO(this, "config", this.settings); 54 | } 55 | 56 | addCustomEmotes(event) { 57 | event.custom_emotes = {}; 58 | 59 | this.settings.custom_emotes.forEach((emote) => { 60 | event.custom_emotes[emote.name] = emote.link; 61 | }) 62 | } 63 | 64 | settingsDisplay = { 65 | "life_time (Seconds)": "number", 66 | max_emojis: "number", 67 | size: "number", 68 | speed: "range", 69 | custom_emotes: "dynamic" 70 | }; 71 | 72 | defaultSettings = { 73 | "life_time (Seconds)": 10, 74 | max_emojis: 1000, 75 | size: 20, 76 | speed: .5, 77 | custom_emotes: { 78 | display: { 79 | name: "input", 80 | link: "input" 81 | }, 82 | default: { 83 | name: "Casterlabs", 84 | link: "https://assets.casterlabs.co/logo/casterlabs_icon.png" 85 | } 86 | } 87 | }; 88 | 89 | }; 90 | -------------------------------------------------------------------------------- /app/modules/modules/recentdonation.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_recent_donation"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_recent_donation"; 6 | this.displayname = "caffeinated.recent_donation.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | this.supportedPlatforms = ["TWITCH", "CAFFEINE", "TROVO"]; 10 | } 11 | 12 | widgetDisplay = [ 13 | { 14 | name: "Reset", 15 | icon: "trash", 16 | async onclick(instance) { 17 | instance.username = null; 18 | instance.amount = 0; 19 | 20 | instance.update(); 21 | MODULES.saveToStore(instance); 22 | } 23 | }, 24 | { 25 | name: "Copy", 26 | icon: "copy", 27 | onclick(instance) { 28 | putInClipboard("https://caffeinated.casterlabs.co/display.html?namespace=" + instance.namespace + "&id=" + instance.id); 29 | } 30 | } 31 | ] 32 | 33 | getDataToStore() { 34 | const data = Object.assign({}, this.settings); 35 | 36 | data.amount = this.amount; 37 | data.username = this.username; 38 | 39 | return data; 40 | } 41 | 42 | onConnection(socket) { 43 | this.update(socket); 44 | } 45 | 46 | init() { 47 | this.amount = this.settings.amount; 48 | this.username = this.settings.username; 49 | 50 | koi.addEventListener("donation", async (event) => { 51 | if (!event.isTest) { 52 | let amount = 0; 53 | 54 | for (const donation of event.donations) { 55 | amount += (await convertCurrency(donation.amount, donation.currency, "USD")); 56 | } 57 | 58 | this.username = event.sender.displayname; 59 | this.amount = amount; 60 | 61 | MODULES.saveToStore(this); 62 | this.update(); 63 | } 64 | }); 65 | } 66 | 67 | async onSettingsUpdate() { 68 | this.update(); 69 | } 70 | 71 | async update(socket) { 72 | MODULES.emitIO(this, "config", this.settings, socket); 73 | 74 | if (this.username) { 75 | const amount = await convertAndFormatCurrency(this.amount, "USD", this.settings.currency); 76 | 77 | MODULES.emitIO(this, "event", ` 78 | 79 | ${this.username} ${amount} 80 | 81 | `, socket); 82 | } else { 83 | MODULES.emitIO(this, "event", "", socket); 84 | } 85 | } 86 | 87 | settingsDisplay = { 88 | font: { 89 | display: "generic.font", 90 | type: "font", 91 | isLang: true 92 | }, 93 | font_size: { 94 | display: "generic.font.size", 95 | type: "number", 96 | isLang: true 97 | }, 98 | currency: { 99 | display: "generic.currency", 100 | type: "currency", 101 | isLang: true 102 | }, 103 | text_color: { 104 | display: "generic.text.color", 105 | type: "color", 106 | isLang: true 107 | } 108 | }; 109 | 110 | defaultSettings = { 111 | font: "Poppins", 112 | currency: "USD", 113 | font_size: 24, 114 | text_color: "#FFFFFF" 115 | }; 116 | 117 | }; 118 | -------------------------------------------------------------------------------- /app/modules/modules/recentfollow.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_recent_follow"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_recent_follow"; 6 | this.displayname = "caffeinated.recent_follow.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | 10 | } 11 | 12 | widgetDisplay = [ 13 | { 14 | name: "Reset", 15 | icon: "trash", 16 | async onclick(instance) { 17 | instance.username = null; 18 | 19 | instance.update(); 20 | MODULES.saveToStore(instance); 21 | } 22 | }, 23 | { 24 | name: "Copy", 25 | icon: "copy", 26 | onclick(instance) { 27 | putInClipboard("https://caffeinated.casterlabs.co/display.html?namespace=" + instance.namespace + "&id=" + instance.id); 28 | } 29 | } 30 | ] 31 | 32 | getDataToStore() { 33 | const data = Object.assign({}, this.settings); 34 | 35 | data.username = this.username; 36 | 37 | return data; 38 | } 39 | 40 | onConnection(socket) { 41 | this.update(socket); 42 | } 43 | 44 | init() { 45 | this.username = this.settings.username; 46 | 47 | if (this.username === undefined) { 48 | this.username = ""; 49 | } 50 | 51 | koi.addEventListener("follow", (event) => { 52 | if (!event.isTest) { 53 | this.username = event.follower.displayname; 54 | 55 | MODULES.saveToStore(this); 56 | this.update(); 57 | } 58 | }); 59 | } 60 | 61 | async onSettingsUpdate() { 62 | this.update(); 63 | } 64 | 65 | update(socket) { 66 | MODULES.emitIO(this, "config", this.settings, socket); 67 | 68 | if (this.username) { 69 | MODULES.emitIO(this, "event", ` 70 | 71 | ${this.username} 72 | 73 | `, socket); 74 | } else { 75 | MODULES.emitIO(this, "event", "", socket); 76 | } 77 | } 78 | 79 | settingsDisplay = { 80 | font: { 81 | display: "generic.font", 82 | type: "font", 83 | isLang: true 84 | }, 85 | font_size: { 86 | display: "generic.font.size", 87 | type: "number", 88 | isLang: true 89 | }, 90 | text_color: { 91 | display: "generic.text.color", 92 | type: "color", 93 | isLang: true 94 | } 95 | }; 96 | 97 | defaultSettings = { 98 | font: "Poppins", 99 | font_size: 24, 100 | text_color: "#FFFFFF" 101 | }; 102 | 103 | }; 104 | -------------------------------------------------------------------------------- /app/modules/modules/recentsubscription.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_recent_subscription"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_recent_subscription"; 6 | this.displayname = "caffeinated.recent_subscription.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | this.supportedPlatforms = ["TWITCH", "TROVO", "BRIME", "KO_FI"]; 10 | } 11 | 12 | widgetDisplay = [ 13 | { 14 | name: "Reset", 15 | icon: "trash", 16 | async onclick(instance) { 17 | instance.username = null; 18 | instance.amount = 0; 19 | 20 | instance.update(); 21 | MODULES.saveToStore(instance); 22 | } 23 | }, 24 | { 25 | name: "Copy", 26 | icon: "copy", 27 | onclick(instance) { 28 | putInClipboard("https://caffeinated.casterlabs.co/display.html?namespace=" + instance.namespace + "&id=" + instance.id); 29 | } 30 | } 31 | ] 32 | 33 | getDataToStore() { 34 | const data = Object.assign({}, this.settings); 35 | 36 | data.username = this.username; 37 | 38 | return data; 39 | } 40 | 41 | onConnection(socket) { 42 | this.update(socket); 43 | } 44 | 45 | init() { 46 | this.username = this.settings.username; 47 | 48 | koi.addEventListener("subscription", async (event) => { 49 | if (!event.isTest) { 50 | const profile = event.gift_recipient ?? event.subscriber; 51 | 52 | this.username = profile.displayname; 53 | 54 | MODULES.saveToStore(this); 55 | this.update(); 56 | } 57 | }); 58 | } 59 | 60 | async onSettingsUpdate() { 61 | this.update(); 62 | } 63 | 64 | async update(socket) { 65 | MODULES.emitIO(this, "config", this.settings, socket); 66 | 67 | if (this.username) { 68 | MODULES.emitIO(this, "event", ` 69 | 70 | ${this.username} 71 | 72 | `, socket); 73 | } else { 74 | MODULES.emitIO(this, "event", "", socket); 75 | } 76 | } 77 | 78 | settingsDisplay = { 79 | font: { 80 | display: "generic.font", 81 | type: "font", 82 | isLang: true 83 | }, 84 | font_size: { 85 | display: "generic.font.size", 86 | type: "number", 87 | isLang: true 88 | }, 89 | currency: { 90 | display: "generic.currency", 91 | type: "currency", 92 | isLang: true 93 | }, 94 | text_color: { 95 | display: "generic.text.color", 96 | type: "color", 97 | isLang: true 98 | } 99 | }; 100 | 101 | defaultSettings = { 102 | font: "Poppins", 103 | currency: "USD", 104 | font_size: 24, 105 | text_color: "#FFFFFF" 106 | }; 107 | 108 | }; 109 | -------------------------------------------------------------------------------- /app/modules/modules/subscribercounter.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_subscriber_counter"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_subscriber_counter"; 6 | this.displayname = "caffeinated.subscriber_counter.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | this.supportedPlatforms = ["TWITCH", "TROVO", "BRIME", "GLIMESH"]; 10 | 11 | } 12 | 13 | widgetDisplay = [ 14 | { 15 | name: "Copy", 16 | icon: "copy", 17 | onclick(instance) { 18 | putInClipboard("https://caffeinated.casterlabs.co/display.html?namespace=" + instance.namespace + "&id=" + instance.id); 19 | } 20 | } 21 | ] 22 | 23 | getDataToStore() { 24 | return this.settings; 25 | } 26 | 27 | onConnection(socket) { 28 | this.update(socket); 29 | } 30 | 31 | init() { 32 | koi.addEventListener("user_data", (event) => { 33 | this.update(null, event); 34 | }); 35 | } 36 | 37 | async onSettingsUpdate() { 38 | this.update(); 39 | } 40 | 41 | update(socket, userdata = CAFFEINATED.userdata) { 42 | MODULES.emitIO(this, "config", this.settings, socket); 43 | 44 | if (userdata) { 45 | MODULES.emitIO(this, "event", ` 46 | 47 | ${userdata.streamer.subscriber_count} 48 | 49 | `, socket); 50 | } else { 51 | MODULES.emitIO(this, "event", "", socket); 52 | } 53 | } 54 | 55 | settingsDisplay = { 56 | font: { 57 | display: "generic.font", 58 | type: "font", 59 | isLang: true 60 | }, 61 | font_size: { 62 | display: "generic.font.size", 63 | type: "number", 64 | isLang: true 65 | }, 66 | text_color: { 67 | display: "generic.text.color", 68 | type: "color", 69 | isLang: true 70 | } 71 | }; 72 | 73 | defaultSettings = { 74 | font: "Poppins", 75 | font_size: 24, 76 | text_color: "#FFFFFF" 77 | }; 78 | 79 | }; 80 | -------------------------------------------------------------------------------- /app/modules/modules/subscriptionalert.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_subscription"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_subscription"; 6 | this.displayname = "caffeinated.subscription_alert.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | this.supportedPlatforms = ["TWITCH", "TROVO", "BRIME", "KO_FI"]; 10 | } 11 | 12 | widgetDisplay = [ 13 | { 14 | name: "Test", 15 | icon: "dice", 16 | onclick(instance) { 17 | koi.test("subscription"); 18 | } 19 | }, 20 | { 21 | name: "Copy", 22 | icon: "copy", 23 | onclick(instance) { 24 | putInClipboard("https://caffeinated.casterlabs.co/alert.html?namespace=" + instance.namespace + "&id=" + instance.id); 25 | } 26 | } 27 | ] 28 | 29 | getDataToStore() { 30 | FileStore.setFile(this, "audio_file", this.audio_file); 31 | FileStore.setFile(this, "image_file", this.image_file); 32 | 33 | return nullFields(this.settings, ["audio_file", "image_file"]); 34 | } 35 | 36 | onConnection(socket) { 37 | MODULES.emitIO(this, "config", this.settings, socket); 38 | MODULES.emitIO(this, "audio_file", this.audio_file, socket); 39 | MODULES.emitIO(this, "image_file", this.image_file, socket); 40 | 41 | } 42 | 43 | init() { 44 | koi.addEventListener("subscription", (event) => { 45 | MODULES.emitIO(this, "event", LANG.formatSubscription(event)); 46 | }); 47 | 48 | if (this.settings.audio_file) { 49 | this.audio_file = this.settings.audio_file; 50 | delete this.settings.audio_file; 51 | 52 | MODULES.saveToStore(this); 53 | } else { 54 | this.audio_file = FileStore.getFile(this, "audio_file", this.audio_file); 55 | } 56 | 57 | if (this.settings.image_file) { 58 | this.image_file = this.settings.image_file; 59 | delete this.settings.image_file; 60 | 61 | MODULES.saveToStore(this); 62 | } else { 63 | this.image_file = FileStore.getFile(this, "image_file", this.image_file); 64 | } 65 | } 66 | 67 | async onSettingsUpdate() { 68 | MODULES.emitIO(this, "config", nullFields(this.settings, ["audio_file", "image_file"])); 69 | 70 | if (this.settings.audio_file.files.length > 0) { 71 | this.audio_file = await fileToBase64(this.settings.audio_file, "audio"); 72 | 73 | MODULES.emitIO(this, "audio_file", this.audio_file); 74 | } 75 | 76 | if (this.settings.image_file.files.length > 0) { 77 | this.image_file = await fileToBase64(this.settings.image_file); 78 | 79 | MODULES.emitIO(this, "image_file", this.image_file); 80 | } 81 | } 82 | 83 | settingsDisplay = { 84 | font: { 85 | display: "generic.font", 86 | type: "font", 87 | isLang: true 88 | }, 89 | font_size: { 90 | display: "generic.font.size", 91 | type: "number", 92 | isLang: true 93 | }, 94 | text_color: { 95 | display: "generic.text.color", 96 | type: "color", 97 | isLang: true 98 | }, 99 | volume: { 100 | display: "generic.volume", 101 | type: "range", 102 | isLang: true 103 | }, 104 | enable_audio: { 105 | display: "generic.enable_audio", 106 | type: "checkbox", 107 | isLang: true 108 | }, 109 | use_custom_image: { 110 | display: "generic.use_custom_image", 111 | type: "checkbox", 112 | isLang: true 113 | }, 114 | audio_file: { 115 | display: "generic.audio.file", 116 | type: "file", 117 | isLang: true 118 | }, 119 | image_file: { 120 | display: "generic.image.file", 121 | type: "file", 122 | isLang: true 123 | } 124 | }; 125 | 126 | defaultSettings = { 127 | font: "Poppins", 128 | font_size: "16", 129 | text_color: "#FFFFFF", 130 | volume: 1, 131 | enable_audio: true, 132 | use_custom_image: true, 133 | audio_file: "", 134 | image_file: "" 135 | }; 136 | 137 | }; 138 | -------------------------------------------------------------------------------- /app/modules/modules/subscriptiongoal.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_subscription_goal"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_subscription_goal"; 6 | this.displayname = "caffeinated.subscription_goal.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | this.supportedPlatforms = ["TWITCH", "TROVO", "BRIME", "GLIMESH"]; 10 | } 11 | 12 | widgetDisplay = [ 13 | { 14 | name: "Reset", 15 | icon: "trash", 16 | async onclick(instance) { 17 | instance.amount = 0; 18 | 19 | instance.sendUpdates(); 20 | MODULES.saveToStore(instance); 21 | } 22 | }, 23 | { 24 | name: "Copy", 25 | icon: "copy", 26 | onclick(instance) { 27 | putInClipboard("https://caffeinated.casterlabs.co/goal.html?namespace=" + instance.namespace + "&id=" + instance.id); 28 | } 29 | } 30 | ] 31 | 32 | getDataToStore() { 33 | return this.settings; 34 | } 35 | 36 | async onConnection(socket) { 37 | this.sendUpdates(socket); 38 | } 39 | 40 | init() { 41 | if (CAFFEINATED.userdata) { 42 | this.amount = CAFFEINATED.userdata.streamer.subscriber_count; 43 | 44 | if (this.amount == -1) { 45 | this.amount = 0; 46 | } 47 | } else { 48 | this.amount = 0; 49 | } 50 | 51 | koi.addEventListener("user_update", (event) => { 52 | this.amount = event.streamer.subscriber_count; 53 | 54 | if (this.amount == -1) { 55 | this.amount = 0; 56 | } 57 | 58 | this.sendUpdates(); 59 | MODULES.saveToStore(this); 60 | }); 61 | } 62 | 63 | onSettingsUpdate() { 64 | this.sendUpdates(); 65 | } 66 | 67 | async sendUpdates(socket) { 68 | MODULES.emitIO(this, "config", this.settings, socket); 69 | 70 | MODULES.emitIO(this, "amount", this.amount, socket); 71 | MODULES.emitIO(this, "display", this.amount, socket); 72 | MODULES.emitIO(this, "goaldisplay", this.settings.goal_amount, socket); 73 | } 74 | 75 | settingsDisplay = { 76 | title: { 77 | display: "caffeinated.generic_goal.name", 78 | type: "input", 79 | isLang: true 80 | }, 81 | goal_amount: { 82 | display: "caffeinated.generic_goal.goal_amount", 83 | type: "number", 84 | isLang: true 85 | }, 86 | height: { 87 | display: "generic.height", 88 | type: "number", 89 | isLang: true 90 | }, 91 | font: { 92 | display: "generic.font", 93 | type: "font", 94 | isLang: true 95 | }, 96 | font_size: { 97 | display: "generic.font.size", 98 | type: "number", 99 | isLang: true 100 | }, 101 | text_color: { 102 | display: "caffeinated.generic_goal.text_color", 103 | type: "color", 104 | isLang: true 105 | }, 106 | bar_color: { 107 | display: "caffeinated.generic_goal.bar_color", 108 | type: "color", 109 | isLang: true 110 | } 111 | }; 112 | 113 | defaultSettings = { 114 | title: "", 115 | goal_amount: 10, 116 | height: 60, 117 | font: "Roboto", 118 | font_size: 28, 119 | text_color: "#FFFFFF", 120 | bar_color: "#31F8FF" 121 | }; 122 | 123 | }; 124 | -------------------------------------------------------------------------------- /app/modules/modules/supporters.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.uniqueModuleClasses["casterlabs_supporters"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_supporters"; 6 | this.displayname = "caffeinated.supporters.title"; 7 | this.type = "application"; 8 | this.id = id; 9 | this.icon = "star"; 10 | this.persist = true; 11 | 12 | } 13 | 14 | init() { 15 | this.page.innerHTML = ` 16 |
17 |

18 | Loving Caffeinated? Feel free to support the project here. 19 |

20 |

21 | Looking for a custom made overlay design?
Get your own at Reyana.org 22 |

23 |
24 |
25 | ★ Our Supporters ★ 26 |
27 |
28 |
29 |
30 |
31 | `; 32 | 33 | setInterval(this.update, 360000); // Every 6 min 34 | 35 | this.update(); 36 | } 37 | 38 | update() { 39 | fetch("https://api.casterlabs.co/v1/caffeinated/supporters").then((response) => response.json()).then((donations) => { 40 | const div = document.querySelector("#supporters"); 41 | 42 | div.innerHTML = ""; 43 | 44 | donations.forEach((donation) => { 45 | const text = document.createElement("pre"); 46 | 47 | text.innerHTML = donation; 48 | text.style = "padding: 0; margin: 0;"; 49 | 50 | div.appendChild(text); 51 | }); 52 | }); 53 | } 54 | 55 | }; 56 | -------------------------------------------------------------------------------- /app/modules/modules/topdonation.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_top_donation"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_top_donation"; 6 | this.displayname = "caffeinated.top_donation.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | this.supportedPlatforms = ["TWITCH", "CAFFEINE", "TROVO"]; 10 | } 11 | 12 | widgetDisplay = [ 13 | { 14 | name: "Reset", 15 | icon: "trash", 16 | async onclick(instance) { 17 | instance.username = null; 18 | instance.amount = 0; 19 | 20 | instance.update(); 21 | MODULES.saveToStore(instance); 22 | } 23 | }, 24 | { 25 | name: "Copy", 26 | icon: "copy", 27 | onclick(instance) { 28 | putInClipboard("https://caffeinated.casterlabs.co/display.html?namespace=" + instance.namespace + "&id=" + instance.id); 29 | } 30 | } 31 | ] 32 | 33 | getDataToStore() { 34 | const data = Object.assign({}, this.settings); 35 | 36 | data.amount = this.amount; 37 | data.username = this.username; 38 | 39 | return data; 40 | } 41 | 42 | onConnection(socket) { 43 | this.update(socket); 44 | } 45 | 46 | init() { 47 | this.amount = this.settings.amount; 48 | this.username = this.settings.username; 49 | 50 | if (this.amount === undefined) { 51 | this.amount = 0; 52 | } 53 | 54 | koi.addEventListener("donation", async (event) => { 55 | if (!event.isTest) { 56 | let amount = 0; 57 | 58 | for (const donation of event.donations) { 59 | amount += (await convertCurrency(donation.amount, donation.currency, "USD")); 60 | } 61 | 62 | if (amount >= this.amount) { 63 | this.username = event.sender.displayname; 64 | this.amount = amount; 65 | 66 | MODULES.saveToStore(this); 67 | this.update(); 68 | } 69 | } 70 | }); 71 | } 72 | 73 | async onSettingsUpdate() { 74 | this.update(); 75 | } 76 | 77 | async update(socket) { 78 | MODULES.emitIO(this, "config", this.settings, socket); 79 | 80 | if (this.username) { 81 | const amount = await convertAndFormatCurrency(this.amount, "USD", this.settings.currency); 82 | 83 | MODULES.emitIO(this, "event", ` 84 | 85 | ${this.username} ${amount} 86 | 87 | `, socket); 88 | } else { 89 | MODULES.emitIO(this, "event", "", socket); 90 | } 91 | } 92 | 93 | settingsDisplay = { 94 | font: { 95 | display: "generic.font", 96 | type: "font", 97 | isLang: true 98 | }, 99 | font_size: { 100 | display: "generic.font.size", 101 | type: "number", 102 | isLang: true 103 | }, 104 | currency: { 105 | display: "generic.currency", 106 | type: "currency", 107 | isLang: true 108 | }, 109 | text_color: { 110 | display: "generic.text.color", 111 | type: "color", 112 | isLang: true 113 | } 114 | }; 115 | 116 | defaultSettings = { 117 | font: "Poppins", 118 | currency: "USD", 119 | font_size: 24, 120 | text_color: "#FFFFFF" 121 | }; 122 | 123 | }; 124 | -------------------------------------------------------------------------------- /app/modules/modules/uptime.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_uptime"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_uptime"; 6 | this.displayname = "caffeinated.uptime.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | } 10 | 11 | widgetDisplay = [ 12 | { 13 | name: "Copy", 14 | icon: "copy", 15 | onclick(instance) { 16 | putInClipboard("https://caffeinated.casterlabs.co/display.html?namespace=" + instance.namespace + "&id=" + instance.id); 17 | } 18 | } 19 | ] 20 | 21 | getDataToStore() { 22 | return this.settings; 23 | } 24 | 25 | onConnection(socket) { 26 | MODULES.emitIO(this, "config", this.settings, socket); 27 | } 28 | 29 | init() { 30 | setInterval(() => this.update(), 1000); 31 | } 32 | 33 | async onSettingsUpdate() { 34 | MODULES.emitIO(this, "config", this.settings); 35 | } 36 | 37 | async update() { 38 | const delta = 39 | (CAFFEINATED.streamdata && CAFFEINATED.streamdata.is_live) ? 40 | (new Date() - new Date(CAFFEINATED.streamdata.start_time)) : 0; 41 | 42 | MODULES.emitIO(this, "event", ` 43 | 44 | ${getFriendlyTime(delta)} 45 | 46 | `); 47 | } 48 | 49 | settingsDisplay = { 50 | font: { 51 | display: "generic.font", 52 | type: "font", 53 | isLang: true 54 | }, 55 | font_size: { 56 | display: "generic.font.size", 57 | type: "number", 58 | isLang: true 59 | }, 60 | text_color: { 61 | display: "generic.text.color", 62 | type: "color", 63 | isLang: true 64 | } 65 | }; 66 | 67 | defaultSettings = { 68 | font: "Poppins", 69 | font_size: 24, 70 | text_color: "#FFFFFF" 71 | }; 72 | 73 | }; 74 | -------------------------------------------------------------------------------- /app/modules/modules/videoshare.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_video_share"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_video_share"; 6 | this.displayname = "caffeinated.videoshare.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | } 10 | 11 | widgetDisplay = [ 12 | { 13 | name: "Copy", 14 | icon: "copy", 15 | onclick(instance) { 16 | putInClipboard("https://caffeinated.casterlabs.co/videoshare.html?id=" + instance.id); 17 | } 18 | } 19 | ] 20 | 21 | getDataToStore() { 22 | return this.settings; 23 | } 24 | 25 | onConnection(socket) { 26 | MODULES.emitIO(this, "config", this.settings, socket); 27 | } 28 | 29 | init() { 30 | const instance = this; 31 | 32 | koi.addEventListener("donation", (event) => { 33 | if (this.settings.enabled) { 34 | MODULES.emitIO(instance, "event", event); 35 | } 36 | }); 37 | 38 | koi.addEventListener("chat", (event) => { 39 | if (this.settings.enabled) { 40 | MODULES.emitIO(instance, "event", event); 41 | } 42 | }); 43 | 44 | } 45 | 46 | onSettingsUpdate() { 47 | MODULES.emitIO(this, "config", this.settings); 48 | } 49 | 50 | settingsDisplay = { 51 | enabled: { 52 | display: "generic.enabled", 53 | type: "checkbox", 54 | isLang: true 55 | }, 56 | player_only: { 57 | display: "caffeinated.videoshare.player_only", 58 | type: "checkbox", 59 | isLang: true 60 | }, 61 | bar_color: { 62 | display: "caffeinated.generic_goal.bar_color", 63 | type: "color", 64 | isLang: true 65 | }, 66 | background_color: { 67 | display: "generic.background.color", 68 | type: "color", 69 | isLang: true 70 | }, 71 | donations_only: { 72 | display: "caffeinated.videoshare.donations_only", 73 | type: "checkbox", 74 | isLang: true 75 | }, 76 | volume: { 77 | display: "generic.volume", 78 | type: "range", 79 | isLang: true 80 | }, 81 | skip: { 82 | display: "caffeinated.videoshare.skip", 83 | type: "button", 84 | isLang: true 85 | }, 86 | pause: { 87 | display: "caffeinated.videoshare.pause", 88 | type: "button", 89 | isLang: true 90 | } 91 | }; 92 | 93 | defaultSettings = { 94 | enabled: true, 95 | player_only: false, 96 | bar_color: "#7a7a7a", 97 | background_color: "#202020", 98 | donations_only: false, 99 | volume: .5, 100 | skip: () => { 101 | MODULES.emitIO(this, "skip", null); 102 | }, 103 | pause: () => { 104 | MODULES.emitIO(this, "pause", null); 105 | } 106 | }; 107 | 108 | }; 109 | -------------------------------------------------------------------------------- /app/modules/modules/viewcounter.js: -------------------------------------------------------------------------------- 1 | 2 | MODULES.moduleClasses["casterlabs_view_counter"] = class { 3 | 4 | constructor(id) { 5 | this.namespace = "casterlabs_view_counter"; 6 | this.displayname = "caffeinated.view_counter.title"; 7 | this.type = "overlay settings"; 8 | this.id = id; 9 | this.supportedPlatforms = ["TWITCH", "CAFFEINE"]; 10 | } 11 | 12 | widgetDisplay = [ 13 | { 14 | name: "Copy", 15 | icon: "copy", 16 | onclick(instance) { 17 | putInClipboard("https://caffeinated.casterlabs.co/display.html?namespace=" + instance.namespace + "&id=" + instance.id); 18 | } 19 | } 20 | ] 21 | 22 | getDataToStore() { 23 | return this.settings; 24 | } 25 | 26 | onConnection(socket) { 27 | this.update(socket); 28 | } 29 | 30 | init() { 31 | this.count = 0; 32 | 33 | koi.addEventListener("viewer_list", (event) => { 34 | this.count = event.viewers.length; 35 | 36 | this.update(); 37 | }); 38 | } 39 | 40 | async onSettingsUpdate() { 41 | this.update(); 42 | } 43 | 44 | update(socket) { 45 | MODULES.emitIO(this, "config", this.settings, socket); 46 | MODULES.emitIO(this, "event", ` 47 | 48 | ${this.count} 49 | 50 | `, socket); 51 | } 52 | 53 | settingsDisplay = { 54 | font: { 55 | display: "generic.font", 56 | type: "font", 57 | isLang: true 58 | }, 59 | font_size: { 60 | display: "generic.font.size", 61 | type: "number", 62 | isLang: true 63 | }, 64 | text_color: { 65 | display: "generic.text.color", 66 | type: "color", 67 | isLang: true 68 | } 69 | }; 70 | 71 | defaultSettings = { 72 | font: "Poppins", 73 | font_size: 24, 74 | text_color: "#FFFFFF" 75 | }; 76 | 77 | }; 78 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Caffeinated", 3 | "version": "1.0.0-beta", 4 | "license": "MIT", 5 | "author": "HelvijsAdams ", 6 | "main": "main.js", 7 | "repository": "https://github.com/thehelvijs/Caffeinated", 8 | "scripts": { 9 | "postinstall": "install-app-deps", 10 | "start": "npm install && electron .", 11 | "pack": "electon-builder --dir", 12 | "dist": "electron-builder" 13 | }, 14 | "build": { 15 | "icon": "media/app_icon.icns", 16 | "appId": "casterlabs.caffeinated", 17 | "win": { 18 | "icon": "media/app_icon.ico", 19 | "target": [ 20 | { 21 | "target": "dir", 22 | "arch": [ 23 | "x64" 24 | ] 25 | } 26 | ] 27 | } 28 | }, 29 | "dependencies": { 30 | "cors": "^2.8.5", 31 | "discord-rpc": "^3.2.0", 32 | "electron-store": "^6.0.1", 33 | "electron-window-state": "^5.0.3", 34 | "express": "^4.17.1", 35 | "font-list": "^1.2.11", 36 | "socket.io": "^2.3.0" 37 | }, 38 | "devDependencies": { 39 | "electron-builder": "v22.9.1", 40 | "electron": "^v11.2.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/uri.all.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "file": "uri.all.js", 4 | "sourceRoot": "", 5 | "sources": [], 6 | "names": [], 7 | "mappings": "" 8 | } -------------------------------------------------------------------------------- /dock/dock.js: -------------------------------------------------------------------------------- 1 | const vars = (() => { 2 | let vars = {}; 3 | 4 | window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, (m, key, value) => { 5 | vars[key] = value; 6 | }); 7 | 8 | return vars; 9 | })(); 10 | 11 | const contentScreenContainer = document.querySelector("#content"); 12 | const connectingScreenContainer = document.querySelector("#connecting"); 13 | let frame; 14 | 15 | const ANIMATION_TIME = 200; 16 | 17 | const UI = { 18 | 19 | showConnectingScreen() { 20 | connectingScreenContainer.classList.remove("hide"); 21 | 22 | anime({ 23 | targets: contentScreenContainer, 24 | easing: "linear", 25 | opacity: 0, 26 | duration: ANIMATION_TIME 27 | }).finished.then(() => { 28 | contentScreenContainer.classList.add("hide"); 29 | contentScreenContainer.innerHTML = ""; 30 | 31 | anime({ 32 | targets: connectingScreenContainer, 33 | easing: "linear", 34 | opacity: 1, 35 | duration: ANIMATION_TIME 36 | }); 37 | }); 38 | }, 39 | 40 | showContentScreen(html) { 41 | frame = document.createElement("iframe"); 42 | 43 | contentScreenContainer.innerHTML = ""; 44 | contentScreenContainer.appendChild(frame); 45 | 46 | // Inject us in. 47 | frame.contentWindow.conn = conn; 48 | 49 | frame.contentDocument.open(); 50 | frame.contentDocument.write(html); 51 | frame.contentDocument.close(); 52 | 53 | contentScreenContainer.classList.remove("hide"); 54 | 55 | anime({ 56 | targets: connectingScreenContainer, 57 | easing: "linear", 58 | opacity: 0, 59 | duration: ANIMATION_TIME 60 | }).finished.then(() => { 61 | connectingScreenContainer.classList.add("hide"); 62 | 63 | anime({ 64 | targets: contentScreenContainer, 65 | easing: "linear", 66 | opacity: 1, 67 | duration: ANIMATION_TIME 68 | }); 69 | }); 70 | } 71 | 72 | }; 73 | 74 | class ConnectionUtil { 75 | constructor() { 76 | this.listeners = []; 77 | 78 | this.namespace = vars.namespace; 79 | this.id = vars.id; 80 | this.uuid = `${vars.namespace}:${vars.id}`; 81 | this.type = vars.type; 82 | 83 | const port = vars.port ? vars.port : 8091; 84 | const ip = vars.address ? vars.address : "http://localhost"; 85 | 86 | // Give some time 87 | setTimeout(() => { 88 | this.socket = io(`${ip}:${port}`, { 89 | reconnection: true, 90 | reconnectionDelay: 1000, 91 | reconnectionDelayMax: 1000, 92 | reconnectionAttempts: Number.MAX_SAFE_INTEGER 93 | }); 94 | 95 | this.socket.on("init", () => { 96 | console.debug("Connected, sending init.") 97 | this.socket.emit("dock-uuid", { 98 | uuid: this.uuid, 99 | type: this.type 100 | }); 101 | }); 102 | 103 | this.socket.on("disconnect", () => { 104 | console.debug("Disconnected.") 105 | UI.showConnectingScreen(); 106 | 107 | for (const listener of this.listeners) { 108 | this.socket.off(listener); 109 | } 110 | 111 | this.listeners = []; 112 | }); 113 | 114 | this.socket.on(`${this.uuid} html`, (html) => { 115 | UI.showContentScreen(html); 116 | }); 117 | }, 600); 118 | } 119 | 120 | on(channel, callback) { 121 | channel = `${this.uuid} ${channel}`; 122 | 123 | this.listeners.push(channel); 124 | this.socket.on(channel, callback); 125 | } 126 | 127 | emit(channel, data) { 128 | channel = `${this.uuid} ${channel}`; 129 | 130 | this.socket.emit(channel, data); 131 | } 132 | 133 | } -------------------------------------------------------------------------------- /dock/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Casterlabs Caffeinated Dock 10 | 11 | 12 | 13 | 14 | 15 |
16 | Connecting to Caffeinated 17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 |
25 | 26 | 27 | 34 | 35 | -------------------------------------------------------------------------------- /dock/spinkit.css: -------------------------------------------------------------------------------- 1 | /* https://tobiasahlin.com/spinkit/ */ 2 | 3 | .spinner { 4 | margin: 30px auto 0; 5 | width: 70px; 6 | text-align: center; 7 | } 8 | 9 | .spinner>div { 10 | width: 18px; 11 | height: 18px; 12 | background-color: #333; 13 | 14 | border-radius: 100%; 15 | display: inline-block; 16 | -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; 17 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 18 | } 19 | 20 | .spinner .bounce1 { 21 | -webkit-animation-delay: -0.32s; 22 | animation-delay: -0.32s; 23 | } 24 | 25 | .spinner .bounce2 { 26 | -webkit-animation-delay: -0.16s; 27 | animation-delay: -0.16s; 28 | } 29 | 30 | @-webkit-keyframes sk-bouncedelay { 31 | 32 | 0%, 33 | 80%, 34 | 100% { 35 | -webkit-transform: scale(0) 36 | } 37 | 38 | 40% { 39 | -webkit-transform: scale(1.0) 40 | } 41 | } 42 | 43 | @keyframes sk-bouncedelay { 44 | 45 | 0%, 46 | 80%, 47 | 100% { 48 | -webkit-transform: scale(0); 49 | transform: scale(0); 50 | } 51 | 52 | 40% { 53 | -webkit-transform: scale(1.0); 54 | transform: scale(1.0); 55 | } 56 | } -------------------------------------------------------------------------------- /dock/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --casterlabs: #e94b4b; 3 | --menu-width: 200px; 4 | --user-bar-height: 55px; 5 | --title-bar-height: 30px; 6 | 7 | --theme: #e94b4b; 8 | --menu-icon-color: #DCDCDC; 9 | --menu-icon-color-hover: #7a7a7a; 10 | --menu-background-color: #202020; 11 | --menu-border-color: #383838; 12 | --background-color: #141414; 13 | --box-background-color: #181818; 14 | --dynamic-option-background-color: #222222; 15 | } 16 | 17 | html { 18 | color: whitesmoke; 19 | background-color: var(--background-color); 20 | height: 100%; 21 | width: 100%; 22 | overflow: hidden; 23 | } 24 | 25 | /* Helpers */ 26 | 27 | .hide { 28 | display: none; 29 | } 30 | 31 | .center { 32 | position: absolute; 33 | left: 50%; 34 | top: 50%; 35 | transform: translate(-50%, -50%); 36 | } 37 | 38 | 39 | /* Connecting */ 40 | 41 | #connecting { 42 | user-select: none; 43 | width: 50vw; 44 | text-align: center; 45 | } 46 | 47 | iframe { 48 | width: 100vw; 49 | height: 100vh; 50 | position: absolute; 51 | left: 0; 52 | top: 0; 53 | } -------------------------------------------------------------------------------- /package.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | cd app 4 | npm run dist 5 | pause -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | cd app 2 | npm run dist -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | cd app 4 | npm start -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | cd app 2 | npm dist -------------------------------------------------------------------------------- /widgets/alert.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 41 | Caffeinated Alert 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 172 | 173 | -------------------------------------------------------------------------------- /widgets/display.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Caffeinated (undefined) 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 52 | 53 | -------------------------------------------------------------------------------- /widgets/emoji.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 19 | Emoji Rain 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 178 | 179 | -------------------------------------------------------------------------------- /widgets/goal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Caffeinated Goal 6 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
80 |
81 |
82 |

83 |

84 |

 

85 |
86 |
87 |
88 |
89 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /widgets/nowplaying.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 97 | Caffeinated Now Playing 98 | 99 | 100 | 101 | 102 | 103 |
104 | 105 |
106 |

107 |

108 |
109 | 110 |
111 | 112 | 113 | 227 | 228 | --------------------------------------------------------------------------------