Last active
March 30, 2025 08:17
-
-
Save daformat/7fd09f72784764a4b4be382b020b0c82 to your computer and use it in GitHub Desktop.
Beep everytime a new row is added in instant db explorer views
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* --------------------------------- | |
* Instantdb new db entry monitoring | |
* --------------------------------- | |
* Chime every time a row is added to the table body, navigate to next page | |
* when available. Designed for instantdb explorer tables, with the $users | |
* table specifically in mind but works for any other table | |
* | |
* - go to the explorer page you want to watch | |
* - go to the last page available | |
* - open your console and paste this script (which you should review first for | |
* security reasons, **never** blindly paste code in a secure/authenticated | |
* context) | |
* - you should hear a first chime that confirms the script is running | |
* - the script will chime again anytime a new row is added to the table | |
*/ | |
(function () { | |
let pendingChime = 0; | |
let dequeueTimeout = null; | |
// Create the audio context | |
const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
/** | |
* Trigger a single beep | |
* @param options | |
* @param callback | |
*/ | |
function beep(options, callback) { | |
options = options || {}; | |
const volume = options.volume || 1; | |
const frequency = options.frequency || 196; | |
const type = options.type || "sine"; | |
const duration = options.duration || 500; | |
const delay = options.delay || 0; | |
const harsh = options.harsh || false; | |
setTimeout( | |
function () { | |
const oscillator = audioCtx.createOscillator(); | |
const gainNode = audioCtx.createGain(); | |
oscillator.connect(gainNode); | |
gainNode.connect(audioCtx.destination); | |
oscillator.frequency.value = frequency; | |
oscillator.type = type; | |
if (callback) { | |
oscillator.onended = callback; | |
} | |
if (harsh) { | |
gainNode.gain.value = volume; | |
} else { | |
gainNode.gain.value = 0.00001; | |
gainNode.gain.setTargetAtTime( | |
volume, | |
audioCtx.currentTime, | |
duration / 1000, | |
); | |
gainNode.gain.setTargetAtTime( | |
0.00001, | |
audioCtx.currentTime + duration / 4 / 1000, | |
duration / 4 / 1000, | |
); | |
} | |
oscillator.start(); | |
setTimeout(function () { | |
oscillator.stop(); | |
}, duration); | |
}, | |
delay ? delay : 0, | |
); | |
} | |
/** | |
* Handle the table mutations | |
* @param mutations{MutationRecord[]} | |
*/ | |
function handleMutation(mutations) { | |
for (const mutation of mutations) { | |
if (mutation.type === "childList" && mutation.addedNodes) { | |
const addedRows = [...mutation.addedNodes].filter( | |
(node) => node instanceof HTMLTableRowElement, | |
); | |
const nbNewRows = addedRows.length; | |
if (nbNewRows > 0) { | |
pendingChime += nbNewRows; | |
if (dequeueTimeout === null) { | |
dequeueTimeout = setTimeout(consumeChime, 750); | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Compose beeps to produce a nice chime | |
*/ | |
function chime() { | |
// 196 Hz is the frequency for a G3 | |
// see https://www.seventhstring.com/resources/notefrequencies.html | |
const baseFreq = 196; | |
// 4 / 3 is the ratio for a perfect fourth, so it'll always sound nice | |
// see http://www.sengpielaudio.com/Root-Ratios.gif | |
beep({ | |
frequency: baseFreq, | |
delay: 0, | |
}); | |
beep({ | |
frequency: (baseFreq * 4) / 3, | |
delay: 150, | |
}); | |
beep({ | |
frequency: (((baseFreq * 4) / 3) * 4) / 3, | |
delay: 150 * 2, | |
}); | |
} | |
/** | |
* Dequeue beeps | |
*/ | |
function consumeChime() { | |
if (pendingChime > 0) { | |
pendingChime -= 1; | |
chime(); | |
if (pendingChime > 0) { | |
dequeueTimeout = setTimeout(consumeChime, 750); | |
} else { | |
dequeueTimeout = null; | |
} | |
} | |
} | |
// Get the html elements we're interested in | |
const table = document.querySelector("table"); | |
const tableControls = document.querySelector("div:has(+ div > table)"); | |
const prevNext = tableControls.querySelectorAll("button:has(>svg)"); | |
const prevNextArray = Array.from(prevNext).filter( | |
(button) => button instanceof HTMLButtonElement, | |
); | |
// Listen to disabled attribute change on the next button to automatically | |
// click it when possible | |
const [, next] = prevNextArray; | |
if (prevNextArray.length !== 2) { | |
console.log( | |
"Prev / next buttons detection might be broken, got:", | |
prevNextArray, | |
"assumed next button is", | |
next, | |
); | |
} | |
if (next instanceof HTMLButtonElement) { | |
const handleClickNext = () => { | |
if (!next.disabled) { | |
next.click(); | |
} | |
}; | |
const observer = new MutationObserver(handleClickNext); | |
observer.observe(next, { attributes: true }); | |
} | |
// Listen to the table mutations | |
if (table) { | |
lastNbRows = table.querySelectorAll("tbody > tr").length; | |
const observer = new MutationObserver(handleMutation); | |
observer.observe(table, { childList: true, subtree: true }); | |
} | |
// chime once to confirm the script is installed | |
chime(); | |
console.log('🔔 You will hear that sound every time a new row is added to the table') | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment