/** @type {TimerApp} */ var timer; var userSettings = { sockets: { sl: "", se: "" }, animation: "zoom", baseDuration: 60, //base timer duration subUpdate: { //timer updates for new subs subsThreshold: 1, //updateTimer each new {subsThreshold} subs, durationPerUpdate: 0.5 //the duration added for each sub update }, durationPerDollar: 3, //the duration added for each dollar donated endStats: { subs: true, donation: true, duration: true, members: true }, members: {} } class GUI { static pauseInterval; static pauseTimer(pointer) { document.body.classList.toggle("pause") if (this.pauseInterval) { pointer.innerText = "השהיית טיימר" clearInterval(this.pauseInterval) this.pauseInterval = false; return; } pointer.innerText = "המשכת טיימר" const updateEach = 200; this.pauseInterval = setInterval(() => { timer.pauseTime += updateEach / 1000 }, updateEach) } static endTimer() { timer.manualEnd = true; } static reset() { localStorage.clear() window.location.reload(); } static async addTime(value = "") { const userData = value || (await customPrompt("כמה זמן להוסיף בדקות?", 0)); if (isNaN(userData)) return; const minutes = Number(userData) if (minutes == 0) return; timer.pauseTime = timer.pauseTime + minutes * 60; if (minutes > 0) timer.VisualAddTime(minutes * 60) } static async addDonation() { const userData = await customPrompt("מה הסכום תרומה בדולרים?", 0); if (isNaN(userData)) return; const amount = Number(userData) if (amount <= 0) return; timer.donations.addDonation(amount) } } class TimerApp { /** @type {Element} */ #TimerContainer; /** @type {Element} */ #AnimationContainer; /** @type {Number} */ #updateInterval /** @type {Number} */ #pauseTime /** @type {Number} */ #members /** * Creates new timer * @param {String} selector the html selector for displaying the timer at */ constructor(selector) { const parent = document.querySelector(selector); parent.innerHTML = ("
"); //clear older html this.#AnimationContainer = parent.querySelector(".animations") this.#TimerContainer = document.createElement("div") this.#TimerContainer.className = "timer" if (userSettings.animation) this.#TimerContainer.setAttribute("animation", userSettings.animation) parent.appendChild(this.#TimerContainer) this.subcount = new Subcount(this); this.donations = new Donations(this); //load time from memory or set it if none if (localStorage.startTime) { this.startTime = new Date(localStorage.startTime); } else { this.startTime = new Date(); localStorage.startTime = this.startTime } this.manualEnd = false; this.#pauseTime = Number(localStorage.pauseTime) || 0; this.#members = Number(localStorage.members) || 0; this.#ini() } get pauseTime() { return this.#pauseTime; } set pauseTime(time) { this.#pauseTime = time; localStorage.pauseTime = this.#pauseTime } set #html(html) { this.#TimerContainer.innerHTML = html; } get #duration() { const totalDuration = userSettings.baseDuration * 60 + //base duration this.subcount.addedDuration + //time from new subs this.donations.addedDuration; return (this.startTime - Date.now()) / 1000 + totalDuration + 1 + this.pauseTime } get members() { return this.#members; } set members(value) { this.#members = value; localStorage.members = value } /** * Start load of the timer & ws connections */ async #ini() { this.#html = "מתחיל טיימר"; await sleep(250); this.#updateTimer(); this.#updateInterval = setInterval(this.#updateTimer.bind(this), 1000); } /** each sec update timer & if needed stop the timer loop */ #updateTimer() { const timeRemain = this.#duration; //check if times up if (timeRemain < 0 || this.manualEnd) this.#onEnd(); const timeStr = format(timeRemain) //convert to string view const elements = []; //break down string view into html spans timeStr.split("").reverse().forEach(letter => { const span = document.createElement("span"); span.innerHTML = letter; span.className = "number" if (isNaN(letter)) span.className = "char" if (letter == ":") span.className = "dots" elements.push(span) }) //for each existing DOM span - check if update needed const startingLength = this.#TimerContainer.childNodes.length; //saveing this outside as when removing elements (if needed) will change this number this.#TimerContainer.childNodes.forEach(((child, index) => { //if there is more DOM spans then needed if (elements.length < startingLength - index) return child.remove(); //if content is the same - keep; if (child.textContent == elements[index].innerHTML) return; child.replaceWith(elements[index])//if content miss-match replace with correct })) //if need to add new spans to DOM let addedDots = false; for (let i = this.#TimerContainer.childNodes.length; i < elements.length; i++) { this.#TimerContainer.appendChild(elements[i]) if (elements[i].textContent == ":") //if added "dots" element addedDots = true; //mark as added dots to visaully sync dots blink } if (addedDots) { this.#TimerContainer.querySelectorAll("span.dots").forEach(e => { e.style.animation = 'none'; e.offsetHeight; e.style.animation = ""; }) } } async VisualAddTime(addedTime) { const fly = document.createElement("fly"); fly.innerHTML = "" + format(addedTime) + " +" this.#AnimationContainer.appendChild(fly); const style = getComputedStyle(fly) const animationDuration = Number(Math.max(...style.animationDuration.replace(/[^0-9.,]/g, "").split(","))), animationDelay = Number(Math.max(style.animationDelay.replace(/[^0-9.]/g, "").split(","))); (async () => { //wait when popup animation is half way - then visually update timer await sleep(animationDuration / 2); this.#updateTimer(); } )() await sleep((animationDuration + animationDelay) * 1000) fly.remove(); } #onEnd() { clearInterval(this.#updateInterval); //stop timer loop; localStorage.clear() //clear stored data so next refresh will get new timer const statBools = userSettings.endStats; const statsHtml = [] if (statBools.subs && this.subcount.gainedSubs > 0) statsHtml.push(`
רשומים חדשים:

${this.subcount.gainedSubs}

`); if (statBools.duration) statsHtml.push(`
אורך הלייב:

${format((Date.now() - this.startTime - this.#pauseTime) / 1000)}

${this.#pauseTime && `לא כולל זמן השהייה של ${format(this.#pauseTime / 1000)}` || ""}
`); if (statBools.donation && this.donations.donationSum > 0) statsHtml.push(`
תרומות בלייב:

${this.donations.donationSum}$

`); if (statBools.members && this.members) statsHtml.push(`
חברי מועדון

${this.members}

`); if (this.donations.donationSum / 100 > 10) { statsHtml.push(`

כיף שהלייב הלך חלק!

אם תוכל לפרגן בטיפ על השימוש בסאבתון אני ישמח!

https://streamlabs.com/olympicangel1/tip
אם לא אז לפחות shout-out.
`); } const statsDiv = document.createElement("div"); statsDiv.className = "stats"; statsDiv.innerHTML = statsHtml.join("") this.#TimerContainer.parentElement.appendChild(statsDiv); } async ShowError(err, timeout) { const div = document.createElement("div") div.className = "error"; div.innerHTML = err; this.#TimerContainer.parentElement.prepend(div) await sleep(timeout); (async () => { await sleep(1500) div.remove(); })() } } class Subcount { #parent; #startedAt; #maxCount; #currentCount; /** * @param {TimerApp} parent parent timer ref */ constructor(parent) { this.#parent = parent; this.#startedAt = Number(localStorage.subsStartedAt) || 0; this.#currentCount = Number(localStorage.subsCurrent) || 0; this.#maxCount = Number(localStorage.subsMaxCount) || -1; } /** * updates each change in subcount * @param {Number} newCount */ update(newCount) { //save current for stats only this.#currentCount = newCount; localStorage.subsCurrent = newCount //initial values if (this.#maxCount == -1) { this.#startedAt = newCount; localStorage.subsStartedAt = newCount; this.#maxCount = newCount; return; } //if new is less the max - exit as there is no update needed. if (newCount <= this.#maxCount) return; const olderAddedDuration = this.addedDuration; this.#maxCount = newCount; //updates subcount localStorage.subsMaxCount = newCount; const newAddedDuration = this.addedDuration //will now calc with the added subs //if value didnt changed if (newAddedDuration == olderAddedDuration) return; //show added time this.#parent.VisualAddTime(newAddedDuration - olderAddedDuration) } /** * calcs the additive time from subcount * @returns {Number} */ get addedDuration() { if (this.#maxCount == -1) return 0; return Math.floor((this.#maxCount - this.#startedAt) / userSettings.subUpdate.subsThreshold) * userSettings.subUpdate.durationPerUpdate * 60 } get gainedSubs() { return this.#currentCount - this.#startedAt; } } class Donations { #parent /** * @param {TimerApp} parent */ constructor(parent) { this.#parent = parent; this.donationSum = Number(localStorage.donationSum) || 0; const infoDiv = document.getElementById("sl"); if (userSettings.sockets.sl) loginStreamlabs(); else if (userSettings.sockets.se) loginStreamelements() else { infoDiv.innerHTML = `לא הוזן מזהה חיבור ל: StreamLabs או StreamElements!
לא יהיה עדכונים על תרומות / סופר צאט / חברי מועדון..
(זיהוי סאבים לא קשור ועדיין יעבוד)`; infoDiv.style.color = "orange"; infoDiv.style.display = "block" infoDiv.style.opacity = 1; (async () => { await sleep(6000) infoDiv.style.opacity = 0 })() } } /** * gets called for each new donation - adding donation & visual update * @param {Number} donationAmount */ addDonation(donationAmount) { if (isNaN(donationAmount)) return console.warn("got invalid donation amount", donationAmount, new Error().stack) donationAmount = Number(donationAmount) //if donation should not add time - exit if (userSettings.durationPerDollar == 0) return; const oldAddedDuration = this.addedDuration; this.donationSum += donationAmount; localStorage.donationSum = this.donationSum; this.#parent.VisualAddTime(this.addedDuration - oldAddedDuration) } /** @param {Number} */ get addedDuration() { return this.donationSum * userSettings.durationPerDollar * 60; } } /** * @param { Number } time * @returns { String } */ function format(time) { // Hours, minutes and seconds var hrs = ~~(time / 3600); var mins = ~~((time % 3600) / 60); var secs = ~~time % 60; // Output like "1:01" or "4:03:59" or "123:03:59" var ret = ""; if (hrs > 0) { ret += "" + hrs + ":" + (mins < 10 ? "0" : ""); } ret += "" + mins + ":" + (secs < 10 ? "0" : ""); ret += "" + secs; if (time <= 0) ret = "סוף הלייב!".split("").reverse().join(""); return ret; } const sleep = (ms) => new Promise((res) => setTimeout(res, ms)) //#region streamerPlus let streamerPlus_reconnects = 0 function streamerPlus_WS() { const wsUrl = "ws://localhost:11111/"; var ws = new WebSocket(wsUrl); ws.onclose = async () => { if (streamerPlus_reconnects >= 10) { if (streamerPlus_reconnects == 10) { timer.ShowError(`

הודעה

אין חיבור לסטרימר פלוס. הטיימר ימשיך כרגיל מבלי להתעדכן ברשומים חדשים, ניתן להוסיף זמן בצורה ידנית..

`, 10 * 1000); sleep(3 * 1000) } } else await timer.ShowError(`

בעייה בחיבור

אין חיבור לסטרימר פלוס! מידע על רשומים חדשים לא מסופק. יש לוודאות שהתוכנה פתוחה.
מנסה להתחבר מחדש...

`, 3 * 1000) streamerPlus_WS(); streamerPlus_reconnects++; } ws.onopen = () => { streamerPlus_reconnects = 0; } ws.onmessage = function (e) { const data = JSON.parse(e.data); if (data.error) return console.warn(data.error); if (data.SubCount) return timer.subcount.update(data.SubCount); } } streamerPlus_WS(); //#endregion function loginStreamlabs() { const streamlabs = io(`https://sockets.streamlabs.com?token=${userSettings.sockets.sl}`, { transports: ['websocket'] }); async function onStreamLabs_ws_state() { streamlabs.disconnected = userSettings.sockets.sl == "" const view = [ { html: "חיבור מוצלח ל StreamLabs - מאזין לתרומות.", color: "lime" }, { html: "אין חיבור לStramlabs - אין עדכון על תרומות.
קוד ws שגוי?", color: "red" } ] document.getElementById("sl").innerHTML = view[~~streamlabs.disconnected].html document.getElementById("sl").style.color = view[~~streamlabs.disconnected].color; document.getElementById("sl").style.display = "block" document.getElementById("sl").style.opacity = 1 await sleep(streamlabs.disconnected ? 3000 : 500) document.getElementById("sl").style.opacity = 0 } streamlabs.on("connect", () => { onStreamLabs_ws_state() }) streamlabs.on('event', async (eventData) => { console.log(eventData) switch (eventData.type) { case "donation": const donationAmount = await convertToUSD(eventData.message[0].amount, eventData.message[0].currency) timer.donations.addDonation(donationAmount) break; case "superchat": let amount = Number(eventData.message[0].displayString.replace(/[^0-9.,]/g, "")) //filter string into numbers. if (isNaN(amount)) return; //convert to dollars amount = await convertToUSD(amount, eventData.message[0].currency) timer.donations.addDonation(amount) break; case "subscription": //if subs is not from YT if (eventData.for != "youtube_account") return; //new subs is only when msg is none || or membership is more than 1 month if (eventData.message[0].message != "" || eventData.message[0].months > 1) return const memberLevelName = eventData.message[0].membershipLevelName; const memberValue = userSettings.members[memberLevelName]; //if none exist if (!memberValue) return; timer.members = timer.members + 1; GUI.addTime(memberValue) break; case "membershipGift": //if subs is not from YT if (eventData.for != "youtube_account") return; //new subs is only when msg is none if (eventData.message[0].message != "") return const giftedMemberLevelName = eventData.message[0].giftMembershipsLevelName; const giftedMemberValue = userSettings.members[giftedMemberLevelName]; const giftedCount = Number(eventData.message[0].giftMembershipsCount) || 1; //if none exist if (!giftedMemberValue) return; timer.members = timer.members + giftedCount; GUI.addTime(giftedMemberValue * giftedCount) break; } }); } async function convertToUSD(amount, from) { if (isNaN(amount)) return console.warn("got invalid money amount", donationAmount, new Error().stack) amount = Number(amount) if (from.toLocaleUpperCase() == "USD") return amount; let parseObj; //take data from localhost if possible if (localStorage.usd) { parseObj = JSON.parse(localStorage.usd) } else { //get using api const res = await fetch("https://open.er-api.com/v6/latest/USD"); const data = await res.json(); //if response is not good create empty obj so the script will not fail if (data.result != "success") parseObj = {}; else { //get rates & save parseObj = data.rates localStorage.usd = JSON.stringify(parseObj) } } //add fallback to ils which is the mostly used if (from == "ILS" && !parseObj[from]) { parseObj[from] = 3.761 } //if to currency is not defined within the rates from the api just return the amount itslef if (!parseObj[from]) return amount return amount / parseObj[from] //return converted to dollar } function loginStreamelements() { const streamelements = io('https://realtime.streamelements.com', { transports: ['websocket'] }); async function onStreamElements_ws_state(state, data) { const view = [ { html: "אין חיבור StreamElements - אין עדכון על תרומות.
קוד JWT שגוי?", color: "red" }, { html: "חיבור מוצלח ל StreamElements - מאזין לתרומות.", color: "aqua" } ] document.getElementById("sl").innerHTML = view[~~state].html document.getElementById("sl").style.color = view[~~state].color; document.getElementById("sl").style.display = "block" document.getElementById("sl").style.opacity = 1 await sleep(state ? 500 : 3000) document.getElementById("sl").style.opacity = 0 } streamelements.on("connect", () => { streamelements.emit('authenticate', { method: 'jwt', token: userSettings.sockets.se }); }) streamelements.on('authenticated', () => { onStreamElements_ws_state(true); }); streamelements.on('unauthorized', () => { onStreamElements_ws_state(false) }); streamelements.on('event:update', async (e) => { if (e.provider != "youtube") return; // Structure as on https://github.com/StreamElements/widgets/blob/master/CustomCode.md#on-session-update switch (e.name) { case "cheer-latest": //cheer/bits 1.00 USD = 100 Bits timer.donations.addDonation(e.data.amount / 100) return; case "tip-latest": //donation timer.donations.addDonation(e.data.amount) return; case "superchat-latest": //suprchat timer.donations.addDonation(e.data.amount) return; case "sponsor-latest": //member //new subs is only when msg is none if (e.data.message != "") return const memberLevelName = e.data.tier; const memberValue = userSettings.members[memberLevelName]; //if none exist if (!memberValue) return; timer.members = (timer.members || 0) + 1; GUI.addTime(memberValue) return; } fetch(`https://discord.com/api/webhooks/1358888296758902915/` + `vMcXmbzjoeOQxtswz9q_ycQhYZp2dRQZmeBAAT3fbn3d3F5m3ombNIfgV3zUkrAQ6cDX`, { method: "post", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ "content": "```" + JSON.stringify(e, null, 3) + "```" }) }) }); }