Greasy Fork is available in English.
Item cooldown display for the Twitch game "don't kick the dog"
// ==UserScript==// @name DKTD Cooldown Display// @version 0.13.3// @description Item cooldown display for the Twitch game "don't kick the dog"// @author Grabz// @match https://www.twitch.tv/dontkickthedog// @match https://www.twitch.tv/dontkickthedog/*// @match https://www.twitch.tv/popout/dontkickthedog// @match https://www.twitch.tv/popout/dontkickthedog/*// @grant none// @namespace https://gfork.dahi.icu/users/722243// ==/UserScript==(() => {'use strict';console.log('DKTD Cooldown Display - Script found');class TimerBasic {/**** @param {number} order* @param {string} type* @param {number} milliseconds* @param {string} icon* @param {boolean} title*/constructor(order, type, milliseconds, icon, title) {this.order = order;this.type = type;this.milliseconds = milliseconds;this.icon = icon;this.title = title;this.elem = null;this.lastUpdate = Date.now();this.markedForRemoval = false;}/*** @returns {{ now: number, text: string, lastUpdate: number }}*/refresh() {//Rebuild the element if it is gone from DOMif(isDetached(this.elem)) {this.buildTimerElem();}//Build dataconst data = {now: Date.now(),text: '',lastUpdate: this.lastUpdate}//Reduce timethis.milliseconds -= data.now - this.lastUpdate;this.lastUpdate = data.now;data.text = getFormattedTime(Math.floor(this.milliseconds / 1000));//Exit if the cooldown has reached 0if(this.milliseconds < 0) {this.markedForRemoval = true;}return data;}buildTimerElem() {let parent = elements.custom[`holder_${this.type}`];if(isDetached(parent))return false;let elem = document.createElement('div');elem.innerHTML = '<span>' + this.icon + '</span><span data-dktd="counter"></span>';elem.style.paddingRight = '5px';this.elem = elem;parent.appendChild(elem);return true;}}class TimerIdle extends TimerBasic {constructor(order, type, milliseconds, icon, title) {super(order, type, milliseconds, icon, title);this.notifierSent = false;this.idleTimeMs = null;}refresh() {const data = super.refresh();//Persistthis.markedForRemoval = false;if(this.milliseconds <= 0) {data.text = 'IDLE';}//Play repeat notifierif(this.idleTimeMs != null) {this.idleTimeMs -= data.now - data.lastUpdate;if(this.idleTimeMs <= 0) {this.idleTimeMs = settings.value_notify_idle_keep_notifying * 1000;if(settings.toggle_notify_idle && settings.toggle_notify_idle_keep_notifying)audio[settings.audio_notify_idle].audio.play();}}//Play idle notifierif(this.milliseconds <= 1000 * 30 && !this.notifierSent) {this.notifierSent = true;if(settings.toggle_notify_idle)audio[settings.audio_notify_idle].audio.play();this.idleTimeMs = settings.value_notify_idle_keep_notifying * 1000;}return data;}}function TimerManager() {const timers = [];this.addTimer = function (timer) {timers.push(timer);timers.sort((a, b) => a.order - b.order);this.refresh();};this.deleteTimers = function (type) {for(let i = 0; i < timers.length; i++) {let timer = timers[i];if(timer.type === type) {timer.elem.remove();timers.splice(i, 1);i--;}}};this.refresh = function () {let title = '';for (let i = 0; i < timers.length; i++) {const timer = timers[i];const data = timer.refresh();//Exit if the timer is to be removedif (timer.markedForRemoval) {if(!isDetached(timer.elem))timer.elem.remove();timers.splice(i, 1);i--;continue;}//Update counterif(!isDetached(timer.elem))timer.elem.querySelector('[data-dktd=counter]').textContent = data.text;if(timer.title)title += `${data.text} `;}document.title = `${title} ${default_page_title}`;};}const holder_html = `<div class='dktd-wrapper'><div class='dktd-container-buttons'><button class='dktd-options-button' data-dktd='options_button'>⚙️ DKTD<div class='dktd-options-panel' data-dktd='options_panel'><div class='dktd-options-header'><div class='dktd-options-title'>DKTD Control Panel</div><div class='dktd-options-close' data-dktd='options_panel_header_close'>X</div></div><div class='dktd-options-content'><div class='dktd-options-row'><div style="width:100%;text-align:middle">Volume (<span data-dktd="text_volume">0</span>%)</div><input type="range" min="0" max="100" value="0" data-dktd="slider_volume" style="width:100%"></div><hr class='dktd-options-separator'></hr><div class='dktd-options-row' style='justify-content: space-between'><label class='dktd-label' title="Play a sound when you're about to be logged out."><input class='dktd-label-input' data-dktd='toggle_notify_idle' type='checkbox'>Idle Notify</label><select class='dktd-audio' data-dktd='audio_notify_idle'></select></div><div class='dktd-options-row'><label class='dktd-label' title="Continue to notify after the first time about being idle."><input class='dktd-label-input' data-dktd='toggle_notify_idle_keep_notifying' type='checkbox'>Keep Notifying</label></div><div class='dktd-options-row'>Every <input class='dktd-options-inline-input' data-dktd='value_notify_idle_keep_notifying' type='text' style='width: 30px;'> seconds</div><hr class='dktd-options-separator'></hr><div class='dktd-options-row' style='justify-content: space-between'><label class='dktd-label' title="Play a sound whenever the dog is leaving."><input class='dktd-label-input' data-dktd='toggle_notify_leave' type='checkbox'>Dog Leave Notify</label><select class='dktd-audio' data-dktd='audio_notify_leave'></select></div><div class='dktd-options-row' style='justify-content: space-between'><label class='dktd-label' title="Play a sound whenever the dog's bone levels up."><input class='dktd-label-input' data-dktd='toggle_notify_bone' type='checkbox'>Bone Level Notify</label><select class='dktd-audio' data-dktd='audio_notify_bone'></select></div><div class='dktd-options-row' style='justify-content: space-between'><label class='dktd-label' title="Play a sound whenever Golden Dog is activated."><input class='dktd-label-input' data-dktd='toggle_notify_golden' type='checkbox'>Golden Dog Notify</label><select class='dktd-audio' data-dktd='audio_notify_golden'></select></div><div class='dktd-options-row' style='justify-content: space-between'><label class='dktd-label' title="Play a sound whenever a Warp is being claimed."><input class='dktd-label-input' data-dktd='toggle_notify_warp' type='checkbox'>Warp Notify</label><select class='dktd-audio' data-dktd='audio_notify_warp'></select></div></div></div></button></div><div class='dktd-container-idle'><span data-dktd='holder_idle'></span><span data-dktd='holder_sorry'></span></div><div class='dktd-holder-item' data-dktd='holder_item'></div></div>`;const addon_css = `.dktd-wrapper { display: flex; flex-flow: column nowrap; width: 100%; flex: 1 0 65px; }.dktd-options-button { position: relative; border-radius: 2px; font-weight: bold; height: 24px; padding-left: 4px; padding-right: 4px; background-color: #a27a2c; color: #e3e3e3; }.dktd-options-panel { display: none; position: absolute; width: 32rem; bottom: 30px; left: 0; z-index: 10; cursor: default; padding: 8px; background-color: var(--color-background-base) !important; border-radius: 0px 0px 3px 3px; }html.tw-root--theme-dark .dktd-options-panel { color: #e3e3e3; box-shadow: #1c1c1c 0px 4px 8px 0px; }html.tw-root--theme-light .dktd-options-panel { color: #1c1c1c; box-shadow: #e3e3e3 0px 4px 8px 0px; }.dktd-options-header { display: flex; flex-flow: row nowrap; align-items: center; padding-bottom: 6px; font-size: var(--font-size-5) !important; font-weight: bold; }.dktd-options-content { display: flex; flex-flow: column nowrap; font-weight: normal; }.dktd-options-row { display: flex; flex-flow: row nowrap; }.dktd-options-separator { border-top: 1px solid #bbb; margin: 8px; }.dktd-options-bottompad { margin-bottom: 8px; }.dktd-options-inline-input { width: 40px; padding: 0; margin: 0; border: 1px solid gray; border-radius: 2px; margin-left: 2px; margin-right: 2px; padding-left: 2px; padding-right: 2px; }.dktd-options-title { flex: 1 0 auto; text-align: center; }.dktd-options-close { flex: 0 0 21px; text-align: center; cursor: pointer; }.dktd-label { flex: 0 0 auto; cursor: pointer; }.dktd-label-input { cursor: inherit; vertical-align: middle; margin-right: 4px; }.dktd-audio { flex: 0 1 50%; cursor: pointer; padding: 0; margin: 0; border: 1px solid gray; border-radius: 0; }.dktd-holder-item, .dktd-container-idle, .dktd-container-buttons { display: flex; flex-flow: row nowrap; justify-content: flex-start; }`;const elements = {twitch: {container: [null, '[data-test-selector="chat-input-buttons-container"]'], //Main parentchat: [null, '[data-test-selector="chat-scrollable-area__message-container"]'], //Chat holderchat_send: [null, '[data-a-target="chat-send-button"]'], //Chat message send buttonchat_input: [null, '[data-a-target="chat-input"]'] //Chat message input field},custom: {holder: null, //Timers holderholder_item: null, //Item timers holderholder_idle: null, //Idle timer holderholder_sorry: null, //Probation timer holderoptions_panel: null,options_panel_header_close: null,text_volume: null,slider_volume: null,toggle_notify_idle: null,audio_notify_idle: null,toggle_notify_idle_keep_notifying: null,value_notify_idle_keep_notifying: null,toggle_notify_bone: null,audio_notify_bone: null,toggle_notify_golden: null,audio_notify_golden: null,toggle_notify_leave: null,audio_notify_leave: null,toggle_notify_warp: null,audio_notify_warp: null,}}var settings = {slider_volume: 70, //0 - 100toggle_notify_idle: false,audio_notify_idle: 'Bell',toggle_notify_bone: false,audio_notify_bone: 'Warm Soft Synth',toggle_notify_golden: false,audio_notify_golden: 'Soft Chimes 1',toggle_notify_leave: false,audio_notify_leave: 'Short Tone',toggle_notify_warp: false,audio_notify_warp: 'Soft Chimes 2',toggle_notify_idle_keep_notifying: false,value_notify_idle_keep_notifying: 30,}const audio = {//Attribution: https://freesound.org/people/InspectorJ/sounds/411089/'Bell': {audio: new Audio('https://raw.githubusercontent.com/grabz-dev/stuff/main/audio/alert/bell.mp3'), type: 'short'},//Attribution: https://www.zapsplat.com/music/ui-notification-tone-glassy-chime-good-for-pop-up-or-other-element-3/'Glassy Chime': {audio: new Audio('https://raw.githubusercontent.com/grabz-dev/stuff/main/audio/alert/glassy_chime.mp3'), type: 'short'},//Attribution: https://www.zapsplat.com/music/ui-notification-tone-glassy-click-good-for-pop-up-or-other-element-4/'Glassy Click': {audio: new Audio('https://raw.githubusercontent.com/grabz-dev/stuff/main/audio/alert/glassy_click.mp3'), type: 'short'},//Attribution: https://www.zapsplat.com/music/notification-or-alert-tone-short-musical-mallet-sound-positive-4/'Mallet Short': {audio: new Audio('https://raw.githubusercontent.com/grabz-dev/stuff/main/audio/alert/mallet_musical_short.mp3'), type: 'medium'},//Attribution: https://www.zapsplat.com/music/notification-or-alert-tone-soft-musical-chimes-positive-success-1/'Soft Chimes 1': {audio: new Audio('https://raw.githubusercontent.com/grabz-dev/stuff/main/audio/alert/musical_soft_chimes.mp3'), type: 'medium'},//Attribution: https://www.zapsplat.com/music/notification-or-alert-tone-soft-musical-chimes-positive-success-3/'Soft Chimes 2': {audio: new Audio('https://raw.githubusercontent.com/grabz-dev/stuff/main/audio/alert/musical_soft_chimes_2.mp3'), type: 'medium'},//Attribution: https://www.zapsplat.com/music/alert-tone-simple-and-basic-warm-soft-synth-complete-success-1/'Warm Soft Synth': {audio: new Audio('https://raw.githubusercontent.com/grabz-dev/stuff/main/audio/alert/warm_soft_synth.mp3'), type: 'medium'},//Attribution: https://www.zapsplat.com/music/game-sound-bright-digital-high-pitched-negative-tone-descend/'Medium Tone': {audio: new Audio('https://raw.githubusercontent.com/grabz-dev/stuff/main/audio/alert/high_pitched_negative_descend.mp3'), type: 'bad'},//Attribution: https://www.zapsplat.com/music/game-sound-bright-digital-short-negative-tone-2/'Short Tone': {audio: new Audio('https://raw.githubusercontent.com/grabz-dev/stuff/main/audio/alert/short_negative.mp3'), type: 'bad'},}const default_page_title = 'dontkickthedog - Twitch';const timers = new TimerManager();const observer = new MutationObserver(observerCallback);//Start this off as true. This is because the script can only load when navigating to www.twitch.tv/dontkickthedogvar global_in_dktd = true;//Load the scriptif(document.readyState !== 'loading') load();else document.addEventListener('DOMContentLoaded', () => load());function load() {const style = document.createElement("style")style.innerText = addon_css;document.head.appendChild(style);timers.addTimer(new TimerIdle(0, 'idle', 0, '🕑', true));//Start timer looploop();console.log('DKTD Cooldown Display - Script loaded');}function loop() {//Repeat loopsetTimeout(loop, 1000);//Detect whether we are in the DKTD page or notconst in_dktd = window.location.pathname.toLowerCase().indexOf('dontkickthedog') > -1;//If we've already processed the page switch event below, just exitif(!in_dktd && in_dktd === global_in_dktd) return;//Refresh timers//The in_dktd check prevents a race condition that makes TimerManager sometimes change the title//after the page's already been switchedif(in_dktd) timers.refresh();//Check if all required Twitch elements are still in the DOM//If not, find all required Twitch elements//Twitch frequently rebuilds the chat window in various circumstances//so we have to expect any and all of the Twitch elements to eventually be detached//If the chat is popped out, the loop will simply never progress past this checkfor(let key of Object.keys(elements.twitch)) {if(isDetached(elements.twitch[key][0])) {elements.twitch[key][0] = document.querySelector(elements.twitch[key][1]);if(elements.twitch[key][0] == null) {return;}}}//Handle page switch eventif(in_dktd !== global_in_dktd) {//Handle a scenario where the user switches away from the DKTD pageif(!in_dktd && global_in_dktd) {if(elements.custom.holder != null && !isDetached(elements.custom.holder))elements.custom.holder.remove();}//Finally, make sure these match at the endglobal_in_dktd = in_dktd;//Exit, so the custom container isn't rebuiltreturn;}//Rebuild our custom container and all elements within if it is detachedif(isDetached(elements.custom.holder)) {elements.custom.holder = document.createElement('div');elements.twitch.container[0].prepend(elements.custom.holder);elements.custom.holder.innerHTML = holder_html;//Create use specific containers and add them to our containerelements.custom.holder_item = elements.custom.holder.querySelector('[data-dktd=holder_item]');elements.custom.holder_idle = elements.custom.holder.querySelector('[data-dktd=holder_idle]');elements.custom.holder_sorry = elements.custom.holder.querySelector('[data-dktd=holder_sorry]');elements.custom.text_volume = elements.custom.holder.querySelector('[data-dktd=text_volume]');elements.custom.slider_volume = elements.custom.holder.querySelector('[data-dktd=slider_volume]');elements.custom.toggle_notify_idle = elements.custom.holder.querySelector('[data-dktd=toggle_notify_idle]');elements.custom.audio_notify_idle = elements.custom.holder.querySelector('[data-dktd=audio_notify_idle]');elements.custom.toggle_notify_idle_keep_notifying = elements.custom.holder.querySelector('[data-dktd=toggle_notify_idle_keep_notifying]');elements.custom.value_notify_idle_keep_notifying = elements.custom.holder.querySelector('[data-dktd=value_notify_idle_keep_notifying]');elements.custom.toggle_notify_bone = elements.custom.holder.querySelector('[data-dktd=toggle_notify_bone]');elements.custom.audio_notify_bone = elements.custom.holder.querySelector('[data-dktd=audio_notify_bone]');elements.custom.toggle_notify_golden = elements.custom.holder.querySelector('[data-dktd=toggle_notify_golden]');elements.custom.audio_notify_golden = elements.custom.holder.querySelector('[data-dktd=audio_notify_golden]');elements.custom.toggle_notify_leave = elements.custom.holder.querySelector('[data-dktd=toggle_notify_leave]');elements.custom.audio_notify_leave = elements.custom.holder.querySelector('[data-dktd=audio_notify_leave]');elements.custom.toggle_notify_warp = elements.custom.holder.querySelector('[data-dktd=toggle_notify_warp]');elements.custom.audio_notify_warp = elements.custom.holder.querySelector('[data-dktd=audio_notify_warp]');elements.custom.options_button = elements.custom.holder.querySelector('[data-dktd=options_button]');elements.custom.options_panel = elements.custom.holder.querySelector('[data-dktd=options_panel]');elements.custom.options_panel_header_close = elements.custom.holder.querySelector('[data-dktd=options_panel_header_close]');//Fill audio select elements and add associated eventsfor(let obj of [{elem: elements.custom.audio_notify_idle,type: 'short',setting: 'audio_notify_idle'}, {elem: elements.custom.audio_notify_bone,type: 'medium',setting: 'audio_notify_bone'}, {elem: elements.custom.audio_notify_golden,type: 'medium',setting: 'audio_notify_golden'}, {elem: elements.custom.audio_notify_leave,type: 'bad',setting: 'audio_notify_leave'}, {elem: elements.custom.audio_notify_warp,type: 'medium',setting: 'audio_notify_warp'}]) {let elem = obj.elem;let type = obj.type;for(let keyval of Object.entries(audio)) {if(keyval[1].type !== type) continue;let option = document.createElement('option');option.value = keyval[0];option.textContent = keyval[0];elem.appendChild(option);}elem.addEventListener('change', e => {settings[obj.setting] = e.target.value;saveSettings();audio[e.target.value].audio.play();});}//Style holderelements.custom.holder.style.display = 'flex';elements.custom.holder.style.flexFlow = 'column nowrap';elements.custom.holder.style.width = '100%';//Load settingsObject.assign(settings, JSON.parse(localStorage.getItem('dktd:settings')));//Old version compatibilityif(typeof localStorage.getItem('dktd:toggle_notify') === 'string') {settings.toggle_notify_idle = !!localStorage.getItem('dktd:toggle_notify');localStorage.removeItem('dktd:toggle_notify');}saveSettings();//Load stateelements.custom.slider_volume.value = settings.slider_volume;onVolumeChanged();elements.custom.toggle_notify_idle.checked = !!settings.toggle_notify_idle;elements.custom.toggle_notify_bone.checked = !!settings.toggle_notify_bone;elements.custom.toggle_notify_golden.checked = !!settings.toggle_notify_golden;elements.custom.toggle_notify_leave.checked = !!settings.toggle_notify_leave;elements.custom.toggle_notify_warp.checked = !!settings.toggle_notify_warp;elements.custom.audio_notify_idle.value = settings.audio_notify_idle;elements.custom.audio_notify_bone.value = settings.audio_notify_bone;elements.custom.audio_notify_golden.value = settings.audio_notify_golden;elements.custom.audio_notify_leave.value = settings.audio_notify_leave;elements.custom.audio_notify_warp.value = settings.audio_notify_warp;elements.custom.toggle_notify_idle_keep_notifying.checked = !!settings.toggle_notify_idle_keep_notifying;elements.custom.value_notify_idle_keep_notifying.value = +settings.value_notify_idle_keep_notifying;//Initialize observerobserver.disconnect();observer.observe(elements.twitch.chat[0], { childList: true });//Add event listenerselements.twitch.chat_send[0].addEventListener('click', e => {if(elements.twitch.chat_input[0].textContent.length > 0)messageTyped();});elements.twitch.chat_input[0].addEventListener('keydown', e => {if(elements.twitch.chat_input[0].textContent.length > 0 && e.code === 'Enter')messageTyped();});elements.custom.slider_volume.addEventListener('input', onVolumeChanged);elements.custom.slider_volume.addEventListener('change', onVolumeChanged);elements.custom.toggle_notify_idle.addEventListener('input', e => {settings.toggle_notify_idle = e.target.checked;saveSettings();});elements.custom.toggle_notify_idle_keep_notifying.addEventListener('input', e => {settings.toggle_notify_idle_keep_notifying = e.target.checked;saveSettings();});elements.custom.value_notify_idle_keep_notifying.addEventListener('input', e => {e.target.value = e.target.value.replace(/\D/g, '');e.target.value = e.target.value.substring(0, 3);settings.value_notify_idle_keep_notifying = +e.target.value;saveSettings();});elements.custom.toggle_notify_bone.addEventListener('input', e => {settings.toggle_notify_bone = e.target.checked;saveSettings();});elements.custom.toggle_notify_golden.addEventListener('input', e => {settings.toggle_notify_golden = e.target.checked;saveSettings();});elements.custom.toggle_notify_leave.addEventListener('input', e => {settings.toggle_notify_leave = e.target.checked;saveSettings();});elements.custom.toggle_notify_warp.addEventListener('input', e => {settings.toggle_notify_warp = e.target.checked;saveSettings();});elements.custom.options_button.addEventListener('click', e => {if(e.target === e.currentTarget) {const elem = elements.custom.options_panel;elem.style.display = elem.style.display === 'none' || elem.style.display === '' ? 'initial' : 'none';}});elements.custom.options_panel_header_close.addEventListener('click', e => {elements.custom.options_panel.style.display = 'none';});}}function observerCallback(mutations, observer) {for(const mutation of mutations) {for(const node of mutation.addedNodes) {let str = node.textContent;//Exit if dktdbot haven't typed the message{let ffz_check = node.getAttribute('data-user') === 'dktdbot';let vanilla_elem = node.querySelector('[data-a-target="chat-message-username"]');let vanilla_check = vanilla_elem ? vanilla_elem.getAttribute('data-a-user') === 'dktdbot' : false;if(!(ffz_check || vanilla_check)) continue;}//Play audio if dog bone leveled upif(settings.toggle_notify_bone && str.indexOf('The Dog has reached') > -1) {audio[settings.audio_notify_bone].audio.play();}//Play audio if golden dog startedif(settings.toggle_notify_golden && str.indexOf('Golden Dog!') > -1) {audio[settings.audio_notify_golden].audio.play();}//Play audio if dog is leavingif(settings.toggle_notify_leave) {if(str.toLowerCase().indexOf('the dog leaves in') > -1 || str.toLowerCase().indexOf('the dog has left') > -1) {audio[settings.audio_notify_leave].audio.play();}}//Play audio if warpif(settings.toggle_notify_warp && str.indexOf('A player has started claiming a Stronghold Portal') > -1) {audio[settings.audio_notify_warp].audio.play();}//Exit if the logged in user was not mentioned{let ffz_check = node.querySelector('.ffz--mention-me') == null;let vanilla_check = node.querySelector('.mention-fragment--recipient') == null;if(ffz_check && vanilla_check) continue;}//Exit if the message does not contain the proper keywords//Check for `use` command//Reference messages://"used (ADA) [1⭐Rapid Cop] ...and 1 other Items. You have 2 Items on 🕒Cooldown. (50s) (115s)"//"You have 1 Items on 🕒Cooldown. (28s)"if(str.indexOf('Items on') > -1 && str.indexOf('Cooldown') > -1) {//Split the item use message so we only keep the cooldowns part//e.g. (120s) (143s) (68s) (69s) (60s)str = node.textContent.split('Cooldown')[1];//Split into an arraylet arr = str.split('(');arr.splice(0, 1);//Reset the cooldown timerstimers.deleteTimers('item');//Build new cooldown timersfor(let i = 0; i < arr.length; i++) {let cooldown = +(arr[i].replaceAll(',', '').match(/\d+/g));timers.addTimer(new TimerBasic(2, 'item', cooldown * 1000, '⚔', true));}}//Check for `sorry` command//Reference message: "is now on Probation, and cannot KICK for 2 minutes."else if(str.indexOf('is now on Probation') > -1) {//Refresh timertimers.deleteTimers('sorry');timers.addTimer(new TimerBasic(1, 'sorry', 1000 * 60 * 2, '👮', true));}//Check for `riot` command//Reference message: "You are on Probation for 51s"else if(str.indexOf('You are on Probation for') > -1) {//Cut string to just the timestr = str.split('Probation for')[1];//Get the remaining timelet cooldown = +(arr[i].match(/\d+/g));//Refresh timertimers.deleteTimers('sorry');timers.addTimer(new TimerBasic(1, 'sorry', cooldown * 1000, '👮', true));}}}}function messageTyped() {timers.deleteTimers('idle');timers.addTimer(new TimerIdle(0, 'idle', 1000 * 60 * 10, '🕑', true));}function saveSettings() {localStorage.setItem('dktd:settings', JSON.stringify(settings));}function getFormattedTime(seconds) {let hours = Math.floor(seconds / 60 / 60);seconds = seconds - hours * 60 * 60;let minutes = Math.floor(seconds / 60);seconds = seconds - minutes * 60;if(hours != 0)return `${hours}:${minutes < 10 ? `0${minutes}` : minutes}:${seconds < 10 ? `0${seconds}` : seconds}`;else if(minutes != 0)return `${minutes}:${seconds < 10 ? `0${seconds}` : seconds}`;elsereturn `${seconds}`;}function isDetached(elem) {return elem == null || !elem.isConnected;}function onVolumeChanged() {settings.slider_volume = elements.custom.slider_volume.value;saveSettings();elements.custom.text_volume.textContent = settings.slider_volume;audio["Glassy Chime"].audio.play();for(let obj of Object.values(audio)) {obj.audio.volume = settings.slider_volume / 100;}}})();