diff --git a/index.html b/index.html
index bd0029d8..92c1ad98 100644
--- a/index.html
+++ b/index.html
@@ -8,7 +8,8 @@
-
+
+
@@ -18,7 +19,6 @@
-
diff --git a/static/addons/panel.html b/static/addons/panel.html
deleted file mode 100644
index b8841b5c..00000000
--- a/static/addons/panel.html
+++ /dev/null
@@ -1,18 +0,0 @@
-
diff --git a/static/config.json b/static/config.json
index 06762e80..b9aa3cbe 100644
--- a/static/config.json
+++ b/static/config.json
@@ -18,7 +18,7 @@
"redirectRootLogin": "/main/friends",
"redirectRootNoLogin": "/about",
"showFeaturesPanel": true,
- "showInstanceSpecificPanel": true,
+ "showInstanceSpecificPanel": false,
"sidebarRight": false,
"subjectLineBehavior": "email",
"theme": "paw-catppuccin",
diff --git a/static/addons/rot.css b/static/rot/rot.css
similarity index 97%
rename from static/addons/rot.css
rename to static/rot/rot.css
index 2b5a8060..c9c75ee0 100644
--- a/static/addons/rot.css
+++ b/static/rot/rot.css
@@ -1,7 +1,3 @@
-#music-controls {
- padding: 15px;
-}
-
.music-controls-title {
text-align: left;
font-size: 18px;
@@ -92,15 +88,13 @@ input:checked ~ .mutecheck {
}
.audioControl {
- border: 1.5px solid var(--icon);
border-radius: var(--panelRadius);
padding: 6px;
- margin-top: 13px;
display: flex;
align-items: center;
gap: 2px;
}
-#user-audio-percentage {
+.volume-percentage {
margin-right: 10px;
}
diff --git a/static/rot/rot.js b/static/rot/rot.js
new file mode 100644
index 00000000..da89b267
--- /dev/null
+++ b/static/rot/rot.js
@@ -0,0 +1,386 @@
+// 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");