diff --git a/static/js/rot.js b/static/js/rot.js
new file mode 100644
index 00000000..c3bdf431
--- /dev/null
+++ b/static/js/rot.js
@@ -0,0 +1,302 @@
+// Rot.js
+// one rotten apple spoils the bunch
+
+(() => {
+ //Helper functions
+ function getPromiseFromEvent(item, event) {
+ return new Promise((resolve) => {
+ const listener = () => {
+ item.removeEventListener(event, listener);
+ resolve();
+ }
+ item.addEventListener(event, listener);
+ });
+ }
+
+ // MIT Licensed
+ // Author: jwilson8767
+ function waitUntil(selector) {
+ return new Promise((resolve, reject) => {
+ const el = document.querySelector(selector);
+ if (el) {
+ resolve(el);
+ return;
+ }
+ new MutationObserver((mutationRecords, observer) => {
+ // Query for elements matching the specified selector
+ Array.from(document.querySelectorAll(selector)).forEach((element) => {
+ resolve(element);
+ //Once we have resolved we don't need the observer anymore.
+ observer.disconnect();
+ });
+ }).observe(document.documentElement, {
+ childList: true,
+ subtree: true
+ });
+ });
+ }
+
+ function sex(sex) {
+ return sex // Sex
+ }
+
+ function rand(list) {
+ return list ? list[list.length * Math.random() | 0] : null;
+ }
+
+ //Music functions
+ function setMusic(url) {
+ if (audio.src == url || (!url && audio.src === window.location.toString()))
+ return;
+
+ audio.pause();
+
+ if (url) {
+ console.log("Setting music: " + url);
+ audio.src = url;
+ playMusic();
+ } else {
+ // This line will cause a lot of errors.
+ // Setting src to "" will cause any pending .play()s to fail.
+ audio.src = "";
+ }
+ }
+
+ function playMusic() {
+ //Starts playing the music if it isn't muted and isn't already playing
+ if (audio.src && audio.src != "" && audio.paused && !audio.muted)
+ audio.play().catch(() => getPromiseFromEvent(window, 'click').then(audio.play));
+ }
+
+ function setImage(url) {
+ if (url) {
+ console.log("Setting background: " + url);
+ let apploaded = document.getElementById("app-loaded");
+ if (apploaded)
+ apploaded.style.setProperty("--body-background-image", "url(" + url + ")");
+ else
+ console.log("Couldn't set background, app not yet loaded");
+ }
+ }
+
+ function volumeSet(number) {
+ audio.volume = number; //Set
+ localStorage.audiovolume = number; //Save
+ updateVolumeLabel();
+ }
+ function volumeAdd(number) {
+ volumeSet(Math.min(1, Math.max(0, audio.volume + number)));
+ }
+ function updateVolumeLabel() {
+ document.getElementById("user-audio-percentage").innerHTML = Math.round(audio.volume * 100) + "%";
+ }
+
+ //Registers a mutation observer for an element. Upon triggering and the callback
+ //returning true, observation ceases. All observers are disconnected and the array is cleared.
+ const observers = [];
+ function waitUntilSpecial(selector, callback) {
+ const newObserver = new MutationObserver((mutationRecords, observer) => {
+ // Query for elements matching the specified selector
+ Array.from(document.querySelectorAll(selector)).forEach((element) => {
+ //Callback
+ if (!callback(element))
+ return;
+
+ //Clean up
+ console.log("Cleaning up");
+ for (const o of observers)
+ o.disconnect();
+ observers.length = 0;
+ });
+ });
+ observers.push(newObserver);
+ newObserver.observe(document.documentElement, {
+ childList: true,
+ subtree: true
+ });
+ }
+
+ //Theme application
+ function applyMainTheme() {
+ console.log("Applying main theme");
+ setMusic(null);
+
+ //
+ waitUntilSpecial('meta[name="pageMusic"]', (pageMusic) => {
+ setMusic(pageMusic.content);
+ playMusic();
+ return true;
+ });
+
+ //
+ waitUntilSpecial("#pageMusic", (pageMusic) => {
+ setMusic(pageMusic.getAttribute("href"));
+ playMusic();
+ return true;
+ });
+ }
+
+ function applyUserTheme() {
+ console.log("Applying user theme");
+ setMusic(null);
+
+ //Configure by post
+ waitUntilSpecial(".pin", (pinnedPost) => {
+ if (pinnedPost.nextElementSibling
+ .querySelector(".StatusBody")
+ .querySelector(".text")
+ .innerHTML
+ .replace(/(<([^>]+)>)/ig, '')
+ .search(/profile theming post/ig) == -1)
+ return false;
+
+ const ptp = pinnedPost.nextElementSibling.querySelector(".StatusBody");
+ if (!ptp)
+ return false;
+
+ const musicContainer = ptp.querySelector(".audio-container");
+ if (musicContainer)
+ setMusic(rand(musicContainer.children).src);
+ else
+ setMusic(null);
+
+ const imageContainer = ptp.querySelector(".image-container");
+ if (imageContainer)
+ setImage(rand(imageContainer.getElementsByTagName("img")).src);
+ else
+ setImage(null);
+
+ return musicContainer || imageContainer;
+ });
+
+ //Configure by fields
+ waitUntilSpecial(".user-profile-field-name", () => {
+ const fields = [...document.getElementsByClassName("user-profile-field-name")]
+ if (fields.length == 0)
+ return false;
+
+ const musicFields = fields.filter((x) => x.title.toLowerCase().replace(/\s+/g, '') == "music")
+ if (musicFields.length > 0)
+ setMusic(rand(musicFields).nextElementSibling.title);
+ else
+ setMusic(null);
+
+ const imageFields = fields.filter((x) => x.title.toLowerCase().replace(/\s+/g, '') == "image")
+ if (imageFields.length > 0)
+ setImage(rand(imageFields).nextElementSibling.title);
+ else
+ setImage(null);
+
+ return musicFields.length > 0 || imageFields.length > 0;
+ });
+ }
+
+ //Switch-based monkey patching router bullshit
+ let lastPath = null;
+ function updateRot() {
+ try {
+ let newPath = window.location.pathname;
+ if (lastPath == newPath)
+ return;
+ lastPath = newPath;
+
+ let pathSpl = newPath.split("/");
+ switch (pathSpl.length) {
+ case 1:
+ applyMainTheme(); //Root
+ break;
+
+ case 2:
+ switch (pathSpl[1]) {
+ case "about":
+ case "announcements":
+ case "lists":
+ case "bookmarks":
+ applyMainTheme();
+ break;
+
+ default:
+ applyUserTheme();
+ break;
+ }
+ break;
+
+ case 3:
+ switch (pathSpl[1]) {
+ case "main":
+ applyMainTheme(); //Main timelines
+ break;
+
+ case "users":
+ applyUserTheme();
+ break;
+
+ case "notice":
+ //Continue playing
+ break;
+
+ default:
+ applyMainTheme();
+ break;
+ }
+ break;
+
+ default:
+ applyMainTheme();
+ break;
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ //Rot music player
+ const audio = document.createElement("audio");
+ audio.loop = true;
+ audio.id = "user-music";
+ audio.style = "display:none;";
+ if (localStorage.audiovolume && localStorage.audiovolume >= 0 && localStorage.audiovolume <= 1)
+ audio.volume = localStorage.audiovolume; //Load volume
+ else
+ audio.volume = 0.2; //Default volume
+
+ //Initialize audio controls and event listeners
+ waitUntil("#music-controls").then((controls) => {
+ updateVolumeLabel();
+ controls.querySelector("#music-up").onclick = () => volumeAdd(0.05);
+ controls.querySelector("#music-down").onclick = () => volumeAdd(-0.05);
+ });
+
+ waitUntil("#music-slider").then((slider) => {
+ updateVolumeLabel();
+ slider.oninput = () => volumeSet(slider.value / 100);
+ });
+
+ waitUntil("#music-mute").then((box) => {
+ audio.muted = box.checked = localStorage.audiomuted === "true";
+ box.addEventListener('click', () => {
+ localStorage.audiomuted = audio.muted = box.checked;
+ playMusic();
+ })
+ });
+
+ //Monkey patches and event listeners
+ const oldPushState = history.pushState;
+ history.pushState = function pushState() {
+ const ret = oldPushState.apply(this, arguments);
+ updateRot();
+ return ret;
+ };
+
+ const oldReplaceState = history.replaceState;
+ history.replaceState = function replaceState() {
+ const ret = oldReplaceState.apply(this, arguments);
+ updateRot();
+ return ret;
+ };
+
+ addEventListener('locationchange', updateRot);
+ addEventListener('popstate', updateRot);
+})();
+
+console.log("rot.js loaded");
\ No newline at end of file