// 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); }); } 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 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() { const percentage = `${Math.round(audio.volume * 100)}%`; const txtElm = document.querySelector('.volume-percentage'); if (txtElm) txtElm.innerHTML = percentage; const slider = document.querySelector('.volume-slider'); if (slider) { const thumb = slider.querySelector('.volume-thumb'); const fill = slider.querySelector('.volume-fill'); if (thumb) thumb.style.left = percentage; if (fill) fill.style.width = percentage; } } //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) => { let p = pinnedPost.nextElementSibling; if (!p) return false; let statusBody = p.querySelector(".StatusBody"); if (!statusBody) return false; let txtElm = statusBody.querySelector(".text"); if (!txtElm) return false; if (txtElm.innerHTML.replace(/(<([^>]+)>)/ig, '').search(/profile them/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); return true; } else { const placeholderContainer = ptp.querySelector(".placeholder-container"); if (placeholderContainer && placeholderContainer.href.endsWith(".m4a")) { setMusic(placeholderContainer.href); return true; } else { setMusic(null); return false; } } }); //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); return true; } else { setMusic(null); return false; } }); } //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": applyMainTheme(); //Statuses 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 //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); function createAudioControls() { let NavPanel = document.querySelector('.NavPanel'); let sideDrawer = document.querySelector('.side-drawer'); if (!NavPanel && !sideDrawer) return; if (document.querySelector('.audioControl')) return; //Initialize audio controls and event listeners console.log("Adding music controls"); const panel = document.createElement("div"); panel.className = "panel panel-default"; const panelHeading = document.createElement("div"); panelHeading.className = "panel-heading"; panel.appendChild(panelHeading); const title = document.createElement("div"); title.className = "title"; title.innerText = "Music Controls"; panelHeading.appendChild(title); const panelBody = document.createElement("div"); panelBody.className = "panel-body"; panel.appendChild(panelBody); const audioControl = document.createElement("div"); audioControl.className = "audioControl"; panelBody.appendChild(audioControl); const mutebutton = document.createElement("label"); mutebutton.className = "mutebutton"; audioControl.appendChild(mutebutton); const musicmute = document.createElement("input"); musicmute.className = "music-mute"; musicmute.setAttribute("type", "checkbox"); musicmute.checked = true; mutebutton.appendChild(musicmute); const mutecheck = document.createElement("span"); mutecheck.className = "mutecheck"; mutecheck.title = "Mute music"; mutebutton.appendChild(mutecheck); const volumeSlider = document.createElement("div"); volumeSlider.className = "volume-slider"; audioControl.appendChild(volumeSlider); const volumeTrack = document.createElement("div"); volumeTrack.className = "volume-track"; volumeSlider.appendChild(volumeTrack); const volumeFill = document.createElement("div"); volumeFill.className = "volume-fill"; volumeTrack.appendChild(volumeFill); const volumeThumb = document.createElement("div"); volumeThumb.className = "volume-thumb"; volumeSlider.appendChild(volumeThumb); const volumePercentage = document.createElement("div"); volumePercentage.className = "volume-percentage"; audioControl.appendChild(volumePercentage); if (NavPanel) NavPanel.insertAdjacentElement('afterend', panel); else if (sideDrawer) sideDrawer.insertAdjacentElement('beforeend', audioControl); { let isDragging = false; function updateVolume(e) { const rect = volumeSlider.getBoundingClientRect(); let x = Math.max(0, Math.min(e.clientX - rect.left, rect.width)); let percentage = (x / rect.width) * 100; // Update actual volume volumeSet(percentage / 100); } function onMouseDown(e) { isDragging = true; updateVolume(e); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); } function onMouseMove(e) { if (isDragging) { updateVolume(e); } } function onMouseUp() { isDragging = false; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } volumeSlider.addEventListener('mousedown', onMouseDown); } audio.muted = musicmute.checked = localStorage.audiomuted === "true"; musicmute.addEventListener('click', () => { localStorage.audiomuted = audio.muted = musicmute.checked; playMusic(); }) updateVolumeLabel(); } createAudioControls(); window.addEventListener('resize', createAudioControls); new MutationObserver((mutationRecords, observer) => { createAudioControls(); }).observe(document.body, { childList: true, subtree: true }); })(); console.log("rot.js loaded");