Soundgasm Improvements

Restyles and adds new functionality to Soundgasm --- dark mode/keyboard shortcuts/quick download/and more

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name        Soundgasm Improvements
// @namespace   V.L
// @version     1.0
// @description Restyles and adds new functionality to Soundgasm --- dark mode/keyboard shortcuts/quick download/and more
// @author      Valerio Lyndon
// @homepageURL https://github.com/ValerioLyndon/Soundgasm-Improvements
// @supportURL  https://github.com/ValerioLyndon/Soundgasm-Improvements/issues
// @license     GPL-3.0-only
// @match       https://soundgasm.net/*
// @run-at      document-start
// @grant       GM_getValue
// @grant       GM_setValue
// ==/UserScript==

'use strict';

document.addEventListener("DOMContentLoaded", domLoaded);

// Dark or Light mode

const theme = GM_getValue('theme', 'dark');
document.documentElement.classList.add(theme);

// CSS

const css = document.createElement('style');
css.textContent = `
	html {
		font-size: 1px;

        --icons: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAGQCAYAAADr8i+wAAAACXBIWXMAAAsTAAALEwEAmpwYAAAMjmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOmI2Mjc5ZTQ3LTNiYzAtM2Q0ZC1iNzUyLWRiNmM4OGU5MDliYyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo5N2EzMzA5ZC03NWE4LWRhNDktYjQyNi04NmU1ZDBlM2ZiZTgiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0iOTc4MTgzNEJDNEFBMTFCODE1QzQzMjUyQkJGQkIwNUEiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0iIiB0aWZmOkltYWdlV2lkdGg9IjIwMCIgdGlmZjpJbWFnZUxlbmd0aD0iNDAwIiB0aWZmOlBob3RvbWV0cmljSW50ZXJwcmV0YXRpb249IjIiIHRpZmY6U2FtcGxlc1BlclBpeGVsPSIzIiB0aWZmOlhSZXNvbHV0aW9uPSIxMDAvMSIgdGlmZjpZUmVzb2x1dGlvbj0iMTAwLzEiIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjEiIGV4aWY6RXhpZlZlcnNpb249IjAyMjEiIGV4aWY6Q29sb3JTcGFjZT0iNjU1MzUiIGV4aWY6UGl4ZWxYRGltZW5zaW9uPSIyMDAiIGV4aWY6UGl4ZWxZRGltZW5zaW9uPSI0MDAiIHhtcDpDcmVhdGVEYXRlPSIyMDIyLTAyLTA4VDIwOjExOjU5LTA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMi0wMi0wOFQyMTowMDoyMS0wODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMi0wMi0wOFQyMTowMDoyMS0wODowMCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjM5ZmM5YWM1LTg2NWMtZjU0Mi05MDM5LTUyNTRmMDg3OTA3MSIgc3RFdnQ6d2hlbj0iMjAyMi0wMi0wOFQyMDoxNzoyNS0wODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGltYWdlL2pwZWcgdG8gYXBwbGljYXRpb24vdm5kLmFkb2JlLnBob3Rvc2hvcCIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iZGVyaXZlZCIgc3RFdnQ6cGFyYW1ldGVycz0iY29udmVydGVkIGZyb20gaW1hZ2UvanBlZyB0byBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo3ZWU4ZTcwNS1kYTBjLTI4NDMtYTQ1My05Y2U4ZDdiMjk0ZTIiIHN0RXZ0OndoZW49IjIwMjItMDItMDhUMjA6MTc6MjUtMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjk4ZTlmMDE1LTg3OGMtODc0Yi04MjBkLWE0NjdjZjEzMTZkOCIgc3RFdnQ6d2hlbj0iMjAyMi0wMi0wOFQyMTowMDoyMS0wODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AgdG8gaW1hZ2UvcG5nIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJkZXJpdmVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJjb252ZXJ0ZWQgZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6OTdhMzMwOWQtNzVhOC1kYTQ5LWI0MjYtODZlNWQwZTNmYmU4IiBzdEV2dDp3aGVuPSIyMDIyLTAyLTA4VDIxOjAwOjIxLTA4OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRm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);
	}

	html.dark {
		--background: hsl(0, 0%, 6.5%);
		--foreground-1: hsl(0, 0%, 12%);
		--foreground-bar-2: hsl(0, 0%, 15%);
		--foreground-bar: hsl(0, 0%, 27%);
		--foreground-2: hsl(0, 0%, 17.6%);
		--border: var(--foreground-bar-2);
		--text-low: hsl(0, 0%, 65%);
		--text-medium: hsl(0, 0%, 80%);
		--text-high: hsl(0, 0%, 98%);
		--accent: hsl(310, 30%, 30%);
	}
	html.light {
		--background: hsl(0, 0%, 96%);
		--foreground-1: hsl(0, 0%, 100%);
		--foreground-bar-2: hsl(0, 0%, 13.3%);
		--foreground-bar: hsl(0, 0%, 9%);
		--foreground-2: hsl(0, 0%, 94%);
		--border: hsl(0, 0%, 94%);
		--text-low: hsl(0, 0%, 25%);
		--text-medium: hsl(0, 0%, 7%);
		--text-high: hsl(0, 0%, 0%);
		--accent: hsl(310, 30%, 70%);
	}

	html body {
		min-width: 424rem;
		max-width: 1200rem;
		padding: 40rem 20rem;
		margin: 0 auto;
		background: var(--background);
		font-size: 12rem;
		color: var(--text-low);
	}

	a {
		color: var(--text-medium) !important;
		text-decoration: none;
	} a:hover {
		color: var(--text-high) !important;
	}

	html *::selection {
		background-color: var(--accent);
	}

	body input,
	body textarea {
		background: var(--foreground-2);
		border: 1px solid var(--border);
		color: var(--text-medium);
		resize: vertical;
	}

	body input[type="submit"]:hover,
	body input[type="submit"]:active {
		cursor: pointer;
		border-color: var(--accent);
	}

	/* Header */

	body header {
		min-height: 20rem;
		padding-bottom: 40rem;
		text-align: center;
	}
	nav a {
		display: inline-block;
	}
	body .logo {
		display: none;
	}

	nav a[href="https://soundgasm.net/logout"] {
		font-size: 0;
	}
	nav a[href="https://soundgasm.net/logout"]::before {
		content: "Logout";
		font-size: 16px;
	}

	/* Multiple-page rules */

	body #container,
	.vl-container,
	body .sound-details,
	#jp_container_1,
	body .uploadform,
	body .contactform,
	body .loginform,
	body .signupform,
	body .passwordresetform,
	.vl-sidebar {
		background: var(--foreground-1);
		box-shadow:
			0 2rem 4rem var(--background),
			0 4rem 10rem hsla(0,0%,0%,10%);
		border-color: var(--border);
		margin: 0 auto;
	}

	#container h1,
	body h1 {
		border-color: var(--border);
		color: var(--text-low);
	}

	/* Generic Containers */

	body p.footer {
		border-color: var(--border);
	}

	body #container {
		max-width: 800rem;
	}

	.vl-container {
		max-width: 770rem;
		padding: 15rem;
		border-size: 1rem;
		margin: 30rem auto;
	}

	.vl-container-header {
		padding-bottom: 10rem;
		border-bottom: 1px solid var(--border);
		margin: 0 0 14rem;
		font-size: 16px;
		font-weight: normal;
	}

	.vl-column {
		display: flex;
		flex-flow: column nowrap;
		gap: 5rem 10rem;
		margin-bottom: 5rem;
		align-items: start;
	}

	.vl-link {
		color: var(--text-medium);
	}
	.vl-link:hover {
		color: var(--text-high);
		text-decoration: underline;
	}


	.vl-paragraph {
		margin: 0 0 5rem;
		font-size: 12rem;
		line-height: 1.35;
	}

	/* Home Page */

	.vl-user-list {
		display: grid;
		grid-template-columns: repeat(4, 1fr);
		gap: 5rem;
		justify-items: start;
		align-items: start;
	}

	/* User Page */

	body .sound-details {
		display: grid;
		width: calc(100% - 22rem);
		padding: 10rem;
		border-radius: 4rem;
		margin: 0 auto 12rem;
		grid-template-columns: 1fr 70rem;
		grid-template-rows: auto auto;
		grid-template-areas:
			"title plays"
			"description description";
		grid-auto-flow: column;
	}


	.sound-details > a {
		grid-area: title;
		justify-self: start;
		font-size: 16rem;
		font-weight: bold;
		white-space: normal;
	}

	.playCount {
		grid-area: plays;
		max-width: 70rem;
		margin-left: auto;
		text-align: right;
	}

	.playCount::before {
		content: "";
		display: inline-block;
		border-color: transparent;
		border-left-color: var(--text-low);
		border-style: solid;
		border-width: .45em .65em;
		margin-right: -0.4em;
		vertical-align: middle;
	}

	.soundDescription {
		grid-area: description;
		order: 3;
		width: 100%;
		margin-top: 6rem;
	}

	/* Split Content + Sidebar */

	.vl-split {
		display: grid;
		max-width: calc(640rem + 20rem + 240rem);
		gap: 20rem;
		margin: 0 auto;
	}

	.vl-directory {
		display: grid;
		width: 100vw;
		max-width: 620rem;
		grid-auto-flow: row;
		margin: 0 auto;
	}

	.vl-sidebar {
		padding: 10rem;
		border-radius: 4rem;
		overflow-y: auto;
	}

	@media (max-width: 941px) {
		.vl-split {
			grid-auto-flow: row;
		}
		.vl-sidebar {
			max-width: 600rem;
			max-height: 80vh;
			border: 1px solid var(--border);
		}
	}
	@media (min-width: 940px) {
		.vl-split {
			grid-auto-flow: column;
			align-items: start;
		}
		.vl-sidebar {
			position: sticky;
			top: 20rem;
			width: 220rem;
			max-height: calc(100vh - 160rem);
			order: 1;
		}
	}

	/* Filters */

	.vl-hidden,
	.vl-hidden-by-search,
	.vl-hidden-by-tag {
		display: none !important;
	}

	.vl-filters {
		font-size: 12rem;
	}

	.vl-filter-section {
		margin-bottom: 15rem;
	}

	.vl-filter-header {
		border-bottom: 1rem solid var(--border);
		margin: 0 0 10rem;
		font-size: 14rem;
		line-height: 1.25em;
		font-weight: bold;
	}

	.vl-sort-btn::before {
		content: "• ";
	}

	.vl-sort-btn.is-active {
		font-weight: bold;
	}
	.vl-sort-btn.is-active::after {
		content: attr(data-direction);
		color: var(--text-low);
		font-size: 10rem;
		margin-left: 5rem;
	}

	.vl-search {
		padding: 4rem;
		border-radius: 3px;
		margin: 0 0 5rem;
	}

	.vl-tag-list {
		width: 100%;
		text-align: left;
	}
	.vl-tag-list th {
		font-size: 12rem;
	}
	.vl-tag-list tr > *:last-child {
		text-align: right;
	}

	.vl-tag-btn {
		display: inline;
		padding: 2rem 4rem;
		background: none;
		border: none;
		border-radius: 2.5rem;
		color: var(--text-medium);
		font-size: 11rem;
		text-align: left;
		text-transform: capitalize;
		cursor: pointer;
	}
	.vl-tag-btn.is-active,
	.vl-tag-btn:hover {
		background: var(--foreground-2);
		color: var(--text-high);
	}
	.vl-tag-btn.is-active {
		font-weight: bold;
	}

	.vl-tag-count {
		padding: 1rem;
		font-size: 12rem;
	}

	/* Player Page */

	div[style="margin:10px 0"] {
		margin: 0 0 25rem !important;
		font-size: 18rem;
		text-align: center;
	}

	#jp_container_1,
	.jp-audio .jp-audio-stream,
	.jp-audio .jp-video {
		border: 2rem solid var(--border);
		color: var(--text-low);
	}
	#jp_container_1 {
		width: 420rem;
	}
	.jp-interface {
		background: var(--foreground-bar);
	}
	.jp-audio .jp-details {
		background: var(--foreground-bar-2);
	}
	.jp-details .jp-title {
		font-size: 12rem;
	}
	.light .jp-details .jp-title {
		color: var(--background);
	}
	.jp-description {
		padding: 0 10rem;
		font-size: 12rem;
	}

	/* Player */

	.jp-state-muted .jp-unmute {
		background: url("../image/jplayer.blue.monday.jpg") -60px -170px no-repeat;
	}
	.jp-state-muted .jp-unmute:focus {
		background: url("../image/jplayer.blue.monday.jpg") -79px -170px no-repeat;
	}

	#jp_container_1 button,
	.jp-gui .jp-seek-bar,
	.jp-gui .jp-play-bar,
	.jp-gui .jp-volume-bar,
	.jp-gui .jp-volume-bar-value {
		background-image: var(--icons);
	}

	.jp-gui .jp-progress {
		background: none;
		border-radius: 2.5rem;
	}

	.jp-progress .jp-seeking-bg {
		background: var(--icons) 0 -202px repeat-x;
		animation: seeking .8s ease-in-out infinite alternate;
	}
	@keyframes seeking {
		0% {
			opacity: 1;
		}
		100% {
			opacity: 0.3;
		}
	}

	.jp-gui .jp-volume-controls {
		width: 0;
	}

	.dark .jp-current-time, .dark .jp-duration {
		color: var(--text-medium);
	}
	.light .jp-current-time, .light .jp-duration {
		color: var(--background);
	}

	/* Description */

	.vl-desc-container {
		margin: 12rem 0 0;
	}
	.sound-details .vl-desc-container {
		display: inline;
		margin: 0;
	}

	.vl-desc-new, .vl-desc-raw {
		margin: 12rem 0;
	}
	.sound-details .vl-desc-new, .sound-details .vl-desc-raw:not([style*="none"]) {
		display: inline !important;
		white-space: normal;
	}

	.vl-tag {
		display: inline-block;
		padding: 2rem 4rem;
		background: var(--foreground-2);
		border-radius: 2.5rem;
		margin: 0 4rem 4rem 0;
		color: var(--text-medium);
		font-size: 11rem;
		text-transform: capitalize;
	}

	.vl-showraw {
		display: inline-block;
		opacity: 0.5;
	}
	.vl-showraw:hover {
		opacity: 1;
	}
	.jp-audio .vl-showraw {
		margin-bottom: 12rem;
	}
	.sound-details .vl-showraw {
		float: right;
	}

	/* Contact page */

	header + ul {
		width: 414rem;
		padding-left: 16rem;
		margin: 12rem auto;
		word-break: break-word;
	}


	/* Footer */

	.vl-footer {
		width: 420rem;
		margin: 0 auto;
		text-align: center;
		padding-top: 30rem;
	}

	.vl-footer a {
		padding: 0 15rem;
	}


	/* fixes */
	
	.patreon-widget {
		width: 300px !important;
		height: 36px !important;
	}
`;

document.documentElement.appendChild(css);

// Functions & Classes

function paragraph( text ){
	let p = document.createElement('p');
	p.className = 'vl-paragraph';
	p.textContent = text;
	return p;
}

var users = new class UserDatabase {
	constructor( ){
		let storage = GM_getValue('knownUsernames', '[]');
		this.names = JSON.parse(storage);
	}

	generateUserList( ){
		
	}

	add( name ){
		let index = this.names.indexOf(name);
		if( index === -1 ){
			this.names.push(name);
			this.save();
		}
	}

	remove( name ){
		let index = this.names.indexOf(name);
		if( index > -1 ){
			this.names.splice(index, 1);
			this.save();
		}
	}

	save( ){
		console.log('saving', JSON.stringify(this.names));
		GM_setValue('knownUsernames', JSON.stringify(this.names));
	}
}

class AudioListing {
	constructor({ element, titleSelector, descriptionSelector, playCountSelector = false, order = 0 }){
		// Process description
		let titleDestination = element.querySelector(titleSelector);
		let descDestination = element.querySelector(descriptionSelector);
		let desc = descDestination.textContent;
		let title = titleDestination.textContent;
		let originalTitle = title;
		let originalDesc = desc;
		let processedTitleDiv = document.createElement('span');
		let rawTitleDiv = document.createElement('span');
		let processedDescDiv = document.createElement('div');
		let rawDescDiv = document.createElement('p');
		let tagsDiv = document.createElement('div');
		let descDiv = document.createElement('p');
		let tags = new Set();
		// match all words inside brackets [] {}. also matches parentheses () but only for one-word sections to try and avoid false positives
		const extractTagsRegex = /[\[\{](.*?)[\]\}]|\(([^\s]+)\)/g;
		// same as the extraction regex but with extra whitespace matching rules
		const removeTagsRegex = /\s*(?:[\[\{].*?[\]\}]|\([^\s]+\))\s*/g;

		rawTitleDiv.textContent = title;
		rawTitleDiv.style.display = 'none';

		processedDescDiv.classList.add('vl-desc-container');
		tagsDiv.classList.add('vl-tags');
		descDiv.classList.add('vl-desc-new');
		processedDescDiv.appendChild(tagsDiv);
		processedDescDiv.appendChild(descDiv);

		rawDescDiv.classList.add('vl-desc-raw');
		rawDescDiv.textContent = desc;
		rawDescDiv.style.display = 'none';

		let combined = title + desc;
		var tagMatches = combined.matchAll(extractTagsRegex);

		for( let match of tagMatches ){
			let tag = match[1] === undefined ? match[2] : match[1];
			
			if( tag.length > 0 ){
				tags.add(tag);
			}
		}

		// remove tags from text
		title = title.replaceAll(removeTagsRegex, '')
		desc = desc.replaceAll(removeTagsRegex, '')

		// sort the tags by length
		tags = Array.from(tags);
		tags.sort( (a, b) => { return a.length - b.length; } );

		// create the element
		for( let i = 0; i < tags.length; i++ ){
			var tagSpan = document.createElement('span');
			tagSpan.classList.add('vl-tag');
			tagSpan.textContent = tags[i];
			tagsDiv.appendChild(tagSpan);
		}

		// Create "view raw" button
		var viewRawBtn = document.createElement('a');
		viewRawBtn.href = 'javascript:void(0);';
		viewRawBtn.classList.add('vl-showraw');
		viewRawBtn.textContent = 'Show raw.'
		viewRawBtn.onclick = ()=>{
			if( processedDescDiv.style.display === 'none' ){
				processedTitleDiv.style.display = 'inline';
				rawTitleDiv.style.display = 'none';
				processedDescDiv.style.display = 'block';
				rawDescDiv.style.display = 'none';
				viewRawBtn.textContent = 'Show raw.';
			} else {
				processedTitleDiv.style.display = 'none';
				rawTitleDiv.style.display = 'inline';
				processedDescDiv.style.display = 'none';
				rawDescDiv.style.display = 'block';
				viewRawBtn.textContent = 'Show processed.';
			}
		}

		// finish up with tags & description
		descDiv.textContent = desc.trim();
		processedTitleDiv.textContent = title.trim();

		// Add everything back to DOM unless it is identical
		if( title !== originalTitle || desc !== originalDesc ){
			titleDestination.replaceChildren(processedTitleDiv, rawTitleDiv);
			descDestination.replaceChildren(processedDescDiv, rawDescDiv, viewRawBtn);
		}

		// parse play count and change html
		let plays = false;
		if( playCountSelector ){
			// this if else and element.append are place here due to weird HTML bugs on the default website
			let playElement = element.querySelector(playCountSelector);
			if( !playElement ){
				plays = 0;
				playElement = document.createElement('span');
				playElement.className = 'playCount';
				playElement.textContent = 'unknown';
			}
			else {
				plays = playElement.textContent.split(': ')[1];

				let playText = String(plays);
				if( plays.length > 3 ) {
					playElement.title = `played ${plays} times`;
					playText = playText.substring(0, plays.length - 3) + 'k';
				}
				playElement.textContent = playText;
			}
			element.append(playElement);
		}

		// Assign variables for use in AudioDirectory classes
		this.element = element;
		this.title = title;
		this.description = desc;
		this.plays = plays;
		this.order = order;
		this.tags = tags;
	}
}

class AudioDirectory {
	constructor({ elements, titleSelector, descriptionSelector, playCountSelector = false, filterElement = false }){
		this.audios = []; // used to iterate through audio info
		this.tags = {}; // used as a reference for which items have which tags
		this.selectedTags = []; // used to keep track of current filters
		this.availableElements = []; // used to update tag counts in the sidebar
		this.tagButtons = []; // used to update tag counts in the sidebar

		// process all audios
		for( let index = 0; index < elements.length; index++ ){
			let audio = new AudioListing({
				element: elements[index],
				titleSelector: titleSelector,
				descriptionSelector: descriptionSelector,
				playCountSelector: playCountSelector,
				order: index
			});
			this.audios.push(audio);
			for( let tag of audio.tags ){
				let tagName = tag.toLowerCase();
				if(!( tagName in this.tags )){ this.tags[tagName] = []; }
				this.tags[tagName].push(audio.element);
			}
		}

		// intialise filters and sorting
		if( filterElement ){
			// set up DOM
			this.filterElement = filterElement;
			this.filterElement.classList.add('vl-filters');

			this.sortElement = document.createElement('div');
			this.sortElement.className = 'vl-filter-section';
			this.searchElement = document.createElement('div');
			this.searchElement.className = 'vl-filter-section';
			this.tagElement = document.createElement('div');
			this.tagElement.className = 'vl-filter-section';

			let sortHeader = document.createElement('h6');
			sortHeader.className = 'vl-filter-header';
			sortHeader.textContent = 'Sort by...';
			this.sortElement.append(sortHeader);

			let searchHeader = document.createElement('h6');
			searchHeader.className = 'vl-filter-header';
			searchHeader.textContent = 'Search...';
			this.searchElement.append(searchHeader);

			let tagHeader = document.createElement('h6');
			tagHeader.className = 'vl-filter-header';
			tagHeader.textContent = 'Filter by tag...';
			this.tagElement.append(tagHeader);

			this.sortList = document.createElement('div');
			this.sortList.className = 'vl-column';
			this.sortElement.append(this.sortList);

			this.tagList = document.createElement('table');
			this.tagList.className = 'vl-tag-list';
			this.tagList.insertAdjacentHTML("afterbegin", `
				<tr><th>Name</th><th>Count</th></tr>
			`);
			this.tagElement.append(this.tagList);

			// set up sorting
			this.calculatedSorts = {};
			this.sortButtons = {};

			this.createSortButton('Title', 'title', 'ascending');
			this.createSortButton('Play Count', 'plays', 'descending');
			this.createSortButton('Date', 'order', 'descending');

			this.sort();

			// set up search

			this.searchBar = document.createElement('input');
			this.searchBar.type = 'search';
			this.searchBar.className = 'vl-search';
			this.searchBar.placeholder = 'Search here.';
			this.searchElement.append(this.searchBar);
			this.searchElement.append(paragraph(`Search supports some basic operators. For example: "phrase" to require an exact string or word and -word to excluse a word. Un-quoted words are treated as OR.`));

			this.searchBar.addEventListener('input', ()=>{ this.search() });
			this.searchTimeout = setTimeout(null, 0);

			// set up tag filters

			let sortedTags = Object.keys(this.tags).sort((first,second)=>{
				// sort primarily by number of elements that match the tag with a secondary alphabetical sort 
				let tagCount = this.tags[second].length - this.tags[first].length
				let tagName = first < second ? -1 : first > second ? 1 : 0;
				return tagCount === 0 ? tagName : tagCount;
			});
			for( let tag of sortedTags ){
				this.createTagButton(tag);
			}

			// Append all workspace items to DOM
			this.filterElement.append(this.sortElement, this.searchElement, this.tagElement);
		}

		if( Object.keys(this.tags).length === 0 ){
			this.tagElement.remove();
		}
	}

	createSortButton( title, column, direction ){
		let button = document.createElement('a');
		button.href = 'javascript:void(0);';
		button.textContent = title;
		button.className = 'vl-sort-btn';
		button.dataset.column = column;
		button.dataset.direction = direction;

		button.addEventListener('click', ()=>{
			this.sort(column, direction);
		});
		this.sortList.append(button);
		this.sortButtons[column] = button;
	}

	createTagButton( tag ){
		let row = document.createElement('tr');
		let cell1 = document.createElement('td');
		let cell2 = document.createElement('td');

		let button = document.createElement('button');
		button.type = 'button';
		button.textContent = tag;
		button.className = 'vl-tag-btn';
		cell1.append(button)

		let count = this.tags[tag].length;
		let countElement = document.createElement('span');
		countElement.className = 'vl-tag-count';
		countElement.textContent = count;
		cell2.append(countElement);
		
		row.append(cell1, cell2);
		this.tagList.append(row);

		button.addEventListener('click', ()=>{
			let selected = this.selectedTags.indexOf(tag);
			if( selected > -1 ){
				button.classList.remove('is-active');
				this.selectedTags.splice(selected, 1);
			}
			else {
				button.classList.add('is-active');
				this.selectedTags.push(tag);
			}
			this.applySelectedTags();
		});
		this.tagButtons.push({
			'element': row,
			'tag': tag,
			'countElement': countElement
		});
	}

	sort( column = 'order', direction = 'descending' ){
		// flip direction if already sorting this way
		if( this?.sorted?.column === column && this?.sorted?.direction === direction ){
			direction = direction === 'descending' ? 'ascending' : 'descending';
		}

		this.sorted = { column, direction };

		for( let button of Object.values(this.sortButtons) ){
			if( button.dataset.column === column ){
				button.classList.add('is-active');
				button.dataset.direction = direction;
			}
			else {
				button.classList.remove('is-active');
			}
		}

		// if list was already sorted once, just re-use the previous sort
		let array = [];
		if( `${column}-${direction}` in this.calculatedSorts ){
			array = this.calculatedSorts[`${column}-${direction}`];
		}

		// if not sorted yet, choose correct function and sort
		else {
			// 'order' column gets sorted in reverse due to being front-facingly labelled as date
			let sortFunction = () => { throw new Error('unknown sort'); };
			if( column === 'plays' && direction === 'ascending'
			|| column === 'order' && direction === 'descending' ){
				sortFunction = (first, second) => { return first['value'] - second['value']; };
			}
			else if( column === 'plays' && direction === 'descending'
			|| column === 'order' && direction === 'ascending' ){
				sortFunction = (first, second) => { return second['value'] - first['value']; };
			}
			else if( column === 'title' && direction === 'ascending' ){
				sortFunction = (first, second) => {
					let a = first['value'];
					let b = second['value'];
					return (a < b) ? -1 : (a > b) ? 1 : 0;
				};
			}
			else if( column === 'title' && direction === 'descending' ){
				sortFunction = (first, second) => {
					let a = first['value'].toLowerCase();
					let b = second['value'].toLowerCase();
					return (b < a) ? -1 : (b > a) ? 1 : 0;
				};
			}

			for( let audio of this.audios ){
				array.push({'element': audio.element, 'value': audio[column]});
			}
			array.sort(sortFunction);

			this.calculatedSorts[`${column}-${direction}`] = array;
		}

		// apply sort to items using CSS 'order' values
		for( let i = 0; i < array.length; i++ ){
			array[i]['element'].style.order = i;
		}
	}

	search( ){
		clearTimeout(this.searchTimeout);

		// parse query
		const exclusionRegex = /(?:^|\s)+-(\w+)/g;
		const phraseRegex = /"([^"]+)"/g;
		let query = this.searchBar.value.toLowerCase();

		let failMatches = query.matchAll(exclusionRegex);
		let exclusions = Array.from(failMatches).map( match => match[1] );
		query = query.replaceAll(exclusionRegex, '').trim();

		let phraseMatches = query.matchAll(phraseRegex);
		let phrases = Array.from(phraseMatches).map( match => match[1] );
		query = query.replaceAll(phraseRegex, '').trim();

		let words = query.split(' ');

		this.searchTimeout = setTimeout(()=>{
			for( let audio of this.audios ){
				if( this.passesSearch(audio, phrases, words, exclusions) ){
					audio.element.classList.remove('vl-hidden-by-search');
					this.updateAvailableElements(audio.element);
				}
				else {
					audio.element.classList.add('vl-hidden-by-search');
					this.updateAvailableElements(audio.element);
				}
			}
			this.updateTagCounts();
		}, 350);
	}

	passesSearch( audioListing, phrases, words, exclusions ){
		console.log(phrases, words, exclusions);
		const title = audioListing.title.toLowerCase();
		const tags = audioListing.tags.join(' ').toLowerCase();
		const any = title + tags;

		// cannot match any exclusions
		for( let str of exclusions ){
			if( any.includes(str) ){
				return false;
			}
		}

		// must match all phrases
		for( let phrase of phrases ){
			if(! any.includes(phrase) ){
				return false;
			}
		}

		// can match any word
		for( let word of words ){
			if( any.includes(word) ){
				return true;
			}
		}
	}

	applySelectedTags( ){
		if( this.selectedTags.length === 0 ){
			for( let audio of this.audios ){
				audio.element.classList.remove('vl-hidden-by-tag');
				this.updateAvailableElements( audio.element );
			}
			this.updateTagCounts();
			return;
		}

		for( let audio of this.audios ){
			let passesAllTags = true;
			for( let tag of this.selectedTags ){
				if(! this.tags[tag].includes(audio.element) ){
					passesAllTags = false;
					break;
				}
			}
			if( passesAllTags ){
				audio.element.classList.remove('vl-hidden-by-tag');
				this.updateAvailableElements( audio.element );
			}
			else {
				audio.element.classList.add('vl-hidden-by-tag');
				this.updateAvailableElements( audio.element );
			}
		}
		this.updateTagCounts();
	}

	updateAvailableElements( element ){
		let index = this.availableElements.indexOf(element);
		let hidden = element.classList.contains('vl-hidden-by-tag') | element.classList.contains('vl-hidden-by-search')
		if( hidden && index > -1 ){
			this.availableElements.splice(index, 1);
		}
		else if( !hidden && index === -1 ){
			this.availableElements.push(element);
		}
	}

	updateTagCounts( ){
		for( let data of this.tagButtons ){
			let tag = data.tag;
			let availableCount = 0;
			for( let element of this.availableElements ){
				if( this.tags[tag].includes(element) ){
					availableCount++;
				}
			}

			if( availableCount > 0 ){
				data.countElement.textContent = availableCount;
				data.element.classList.remove('vl-hidden');
			}
			else {
				data.element.classList.add('vl-hidden');
			}
		}
	}
}

// Begin modifying page

function domLoaded() {
	// If content is blank
	let content = document.querySelector('body > div');
	if( content === null ) {
		let blank = document.createElement('div');
		blank.id = 'container';
		blank.innerHTML = `<div id="body"><p>There's nothing here.</p></div>`;
		document.body.appendChild(blank);
	}

	// Add footer
	let footer = document.createElement('footer');
	footer.classList.add('vl-footer');

	// Theme switcher
	let themeSwitcher = document.createElement('a');
	themeSwitcher.textContent = 'Theme';
	themeSwitcher.href = 'javascript:void(0);';
	themeSwitcher.onclick = function() {
		if(GM_getValue('theme', 'dark') === 'dark') {
			GM_setValue('theme', 'light');
			document.documentElement.classList.add('light');
			document.documentElement.classList.remove('dark');
		} else {
			GM_setValue('theme', 'dark');
			document.documentElement.classList.add('dark');
			document.documentElement.classList.remove('light');
		}
	};
	footer.appendChild(themeSwitcher);

	document.body.appendChild(footer);

	var path = window.location.pathname;

	// Per-page sections

	// Any page with a user as long as it has content
	if( content && path.startsWith('/u/') && path.split('/').length > 2 ){
		let username = path.split('/')[2];
		users.add(username);
	}

	// Homepage
	if( path === '/' ){
		document.querySelector('h1').textContent = 'Welcome to Soundgasm.net, improved!';

		let container = document.createElement('div');
		container.className = 'vl-container';
		container.style.marginBottom = '0';
		let header = document.createElement('h3');
		header.className = 'vl-container-header';
		header.textContent = 'Known user list.';
		container.append(header);

		if( users.names.length === 0 ){
			container.append(paragraph(`The script will remember usernames from the audio and user pages you visit. Once you've opened a few, you can always come back here to find them all!`));
		}
		else {
			let userList = document.createElement('div');
			userList.classList.add('vl-user-list');
			userList.style.fontSize = '14rem';

			for( let username of users.names.sort() ){
				let link = document.createElement('a');
				link.href = `/u/${username}`;
				link.textContent = `• ${username}`;
				link.className = 'vl-link';
				userList.append(link);
			}

			container.append(userList);
		}
		footer.insertAdjacentElement('beforebegin', container);
	}

	// User pages
	if( content && path.startsWith('/u/') && path.split('/').length < 4 ){
		// Prep DOM for filters
		let container = document.createElement('div');
		container.className = 'vl-split';

		let directory = document.createElement('main');
		directory.className = 'vl-directory';
		let sidebar = document.createElement('aside');
		sidebar.className = 'vl-sidebar';

		let frag = new DocumentFragment();
		let items = document.querySelectorAll('.sound-details');
		for( let item of items ){
			frag.append(item);
		}
		directory.append(frag);
		container.append(sidebar, directory);
		document.getElementsByTagName('footer')[0].insertAdjacentElement('beforebegin', container);
		
		// catch any oddities such as patron links and other things from descriptions
		let patreon = document.querySelector('.soundDescription .patreon-widget');
		if( patreon ){
			let container = document.createElement('div');
			container.className = 'vl-container';
			container.style.marginTop = '0';
			container.style.width = 'calc(100% - 30rem)';
			let header = document.createElement('h3');
			header.className = 'vl-container-header';
			header.textContent = 'Extra user info.';
			container.append(header, patreon);

			directory.style.order = '-1';
			directory.prepend(container);
		}

		// Process audio listings
		new AudioDirectory({
			elements: items,
			titleSelector: 'a',
			descriptionSelector: '.soundDescription',
			playCountSelector: '.playCount',
			filterElement: sidebar
		});
	}

	// Player page
	if( path.startsWith('/u/') && path.split('/').length > 3 ){
		// Add custom descriptions
		new AudioListing({
			element: document.querySelector('.jp-type-single'),
			titleSelector: '.jp-title',
			descriptionSelector: '.jp-description'
		});

		let stop = document.querySelector('.jp-stop');
		let title = document.querySelector('.jp-title');
		let author = document.querySelector('div[style="margin:10px 0"] a');
		let audio = document.querySelector('audio');

		// Keypress handler
		function setKeybinds() {
			window.addEventListener('keydown', (e) => {
				let k = e.key.toLowerCase();
				if(e.key === ' ') {
					e.preventDefault();
				}
			});

			window.addEventListener('keyup', (e) => {
				let k = e.key.toLowerCase();
				let ctrl = e.ctrlKey;

                let time = 5.0;
                if(ctrl){
                    time = 15.0;
                }

				if(k === 'p' || k === 'k' || k === ' ') {
					if(!audio.paused) {
					    audio.pause();
					} else {
					    audio.play();
					}
				}
				else if(k === 's') {
					stop.click();
				}
				else if(k === 'd') {
					document.querySelector('.dl').click();
				}
				else if(k === 'arrowleft') {
					audio.currentTime -= time;
				}
				else if(k === 'arrowright') {
					audio.currentTime += time;
				}
				else if(k === 'arrowup') {
					let newVol = audio.volume + 0.1;
					if(newVol > 1) {
						newVol = 1.0;
					}
					audio.volume = newVol;
				}
				else if(k === 'arrowdown') {
					let newVol = audio.volume - 0.1;
					if(newVol < 0) {
						newVol = 0.0;
					}
					audio.volume = newVol;
				}
				else if(k === '0') {
					audio.currentTime = 0.0;
				}
				else if(k === '1') {
					audio.currentTime = audio.duration / 10;
				}
				else if(k === '2') {
					audio.currentTime = audio.duration / 10 * 2;
				}
				else if(k === '3') {
					audio.currentTime = audio.duration / 10 * 3;
				}
				else if(k === '4') {
					audio.currentTime = audio.duration / 10 * 4;
				}
				else if(k === '5') {
					audio.currentTime = audio.duration / 10 * 5;
				}
				else if(k === '6') {
					audio.currentTime = audio.duration / 10 * 6;
				}
				else if(k === '7') {
					audio.currentTime = audio.duration / 10 * 7;
				}
				else if(k === '8') {
					audio.currentTime = audio.duration / 10 * 8;
				}
				else if(k === '9') {
					audio.currentTime = audio.duration / 10 * 9;
				}
			});
		}

		// Download button
		function addDownload() {
			let audio = document.querySelector('audio');
			let src = audio.getAttribute('src');
			let ext = src.split('.').pop();
			let dl = document.createElement('a');

			dl.classList.add('dl');
			footer.appendChild(dl);
			dl.href = src;
			dl.setAttribute("download", title.innerText + ' by ' + author.innerText + '.' + ext);
			dl.setAttribute("target", "_blank");
			dl.textContent = 'Download this audio';
		}
		function audioLoaded() {
			audio = document.querySelector('audio');
			if(audio !== null && audio.getAttribute('src') !== null) {
				// observer.disconnect();
				addDownload();
				setKeybinds();
			} else {
				setTimeout(audioLoaded, 100);
			}
		}

		// Wait for audio to load
		if(audio !== null && audio.getAttribute('src') !== null) {
			addDownload();
		} else {
			audioLoaded();
		}
	}

	// signup page
	if(window.location.pathname.startsWith('/signup')) {
		let h1 = document.querySelector('h1');
		let form = document.querySelector('.signupform');
		form.prepend(h1);
	}
}