DEE2 Formatter

Digital Emergency Exit 2 Entry Grid Layout Formatter and Client-Side Sorter

// ==UserScript==
// @name         DEE2 Formatter
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Digital Emergency Exit 2 Entry Grid Layout Formatter and Client-Side Sorter
// @author       Noisysundae
// @match        *manbow.nothing.sh/event/event.cgi*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=nothing.sh
// @grant        none
// @license      MIT
// ==/UserScript==

/* jshint esversion: 11 */

(() => {
	const sortEntries = (by) =>
		entries
			.sort((a, b) => {
				switch (by) {
					case 'index':
						return a[by] - b[by];
					case 'title':
					case 'artists':
					case 'genre':
					case 'production':
						return a[by].localeCompare(b[by]);
					case 'updated':
					case 'impressions':
					case 'total':
					case 'median':
					case 'avg':
					case 'weighted':
					case 'fs':
					case 'dq':
						return b[by] - a[by];
					case 'random':
						return Math.random() > 0.5 ? 1 : -1;
				}
			})
			.reduce(
				(last, current, i, arr) => {
					const lastInfo = {
						order:
							last.value === (current[by] ?? 0) ? last.order : i,
						value: current[by],
					};
					const isRankedSort = [
						'impressions',
						'total',
						'median',
						'avg',
						'weighted',
					].includes(by);
					const topPct =
						((isRankedSort ? lastInfo.order : current.index) /
							arr.length) *
						100;
					list.appendChild(current.element.base);
					if (current.element.ranking)
						current.element.ranking.innerHTML = `${
							isRankedSort
								? `<i class="icon-trophy"></i>${
										lastInfo.order + 1
								  }`
								: `#${current.index}`
						}${
							isRankedSort
								? ' <small>(top' +
								  (topPct
										? ` <b>${topPct.toFixed(2)}</b> %`
										: '') +
								  ')</small>'
								: ''
						}`;
					return lastInfo;
				},
				{ order: -1, value: null }
			);
	const list = document.getElementById('modern_list');
	const teams = document.querySelectorAll('.team_information');
	const entries = Array.from(
		document.querySelectorAll('.team_information .row > div[style]')
	)
		.filter(
			(e) =>
				!e.querySelector('.pricing-title h4.textOverflow span#notready')
		)
		.map((base) => {
			const title = base.querySelector(
				'.pricing-title .center > *:nth-child(2) a'
			);
			const artists = base.querySelector(
				'.pricing-title .center > *:nth-child(3)'
			);
			const genre = base.querySelector(
				'.pricing-title .center > *:nth-child(1)'
			);
			const ranking = base.querySelector(
				'.pricing-features:nth-child(2) .tleft.textOverflow span:nth-child(1)'
			);
			const stats = base.querySelector(
				'.pricing-features > .bofu_meters'
			);
			const info = base.querySelector('.pricing-action small');
			const total = stats.querySelector('p:nth-child(1) > span');
			const avg = stats.querySelector('p:nth-child(3) > span');
			const median = stats.querySelector('p:nth-child(2) > span');

			const entry = {
				element: {
					base,
					title,
					artists,
					genre,
					ranking,
					stats,
					info,
					total,
					median,
					avg,
				},
				index: parseInt(info?.innerHTML.match(/No\.([\d]+?)\ /)[1]),
				title: title.innerHTML,
				artists: artists.innerHTML.replace('(mov:', ' (mov: '),
				genre: genre.innerHTML,
				production: info?.innerHTML.match(/ \/ ([\w]+?)\ /)[1],
				updated: new Date(
					`${info?.innerHTML.match(/update : (.+?)$/)[1]} GMT+9`
				),
				impressions: parseInt(ranking?.innerHTML),
				total: parseFloat(
					(total?.innerHTML.match(/Total : ([\d\.]+?) /) ?? [
						'',
						'0',
					])[1]
				),
				median: parseFloat(
					(median?.innerHTML.match(/Median : ([\d\.]+?) /) ?? [
						'',
						'0',
					])[1]
				),
				avg: 0,
				weighted: 0,
				fs: base.firstElementChild.id === 'ace_mus',
				dq: false,
			};
			if (entry.fs) entry.total *= 2 / 3;
			entry.avg = entry.total / entry.impressions;
			if (entry.total)
				entry.weighted = entry.avg * 0.5 + entry.median * 0.5;
			else entry.dq = true;
			return entry;
		});
	const styles = document.createElement('style');
	const sortLabel = document.createElement('label');
	const sortOptions = document.createElement('select');

	/*
	 * Prefixed by https://autoprefixer.github.io
	 * PostCSS: v8.4.14,
	 * Autoprefixer: v10.4.7
	 * Browsers: last 4 version
	 */
	styles.innerHTML = `
		div#modern_list {
			display: -webkit-box;
			display: -ms-flexbox;
			display: flex;
			-ms-flex-wrap: wrap;
			flex-wrap: wrap;
			-webkit-box-pack: center;
			-ms-flex-pack: center;
			justify-content: center;
		}

		div#modern_list * {
			font-family: 'Yanone Kaffeesatz';
			letter-spacing: 0.03em;
		}

		div#modern_list .label {
			letter-spacing: 0.12em;
			font-size: 0.9em;
		}

		div#modern_list > div {
			width: 8.8cm;
			padding: 0;
		}

		div#modern_list .header_grad .pricing-title > .center {
			display: -webkit-box;
			display: -ms-flexbox;
			display: flex;
			-webkit-box-orient: vertical;
			-webkit-box-direction: normal;
			-ms-flex-direction: column;
			flex-direction: column;
			-webkit-box-align: end;
			-ms-flex-align: end;
			align-items: end;
			font-size: 1.2em;
		}

		div#modern_list .header_grad .pricing-title > .center > * {
			width: 90%;
			text-align: right;
			font-size: 1em;
			letter-spacing: 0.04em;
		}

		div#modern_list
			.header_grad
			.header_alpha
			.pricing-title
			> .center
			> *:nth-child(1) {
			padding-right: 0.6rem;
		}
		div#modern_list
			.header_grad
			.header_alpha
			.pricing-title
			> .center
			> *:nth-child(2) {
			padding-right: 1.4rem;
		}
		div#modern_list
			.header_grad
			.header_alpha
			.pricing-title
			> .center
			> *:nth-child(3) {
			padding-right: 2.2rem;
		}

		h4.textOverflow > strong {
			font-size: 1.4em;
		}

		.bmsinfo.textOverflow {
			text-align: right;
		}

		.header_alpha .bmsinfo.textOverflow {
			padding-right: 3rem;
		}

		.header_grad {
			margin: 0.4em;
			border-radius: 0.8em;
		}

		div#modern_list .pricing-box.best-price,
		.header_grad {
			overflow: hidden;
		}

		div#modern_list .pricing-box.best-price {
			margin: 0.6em;
			padding: 0.4em;
			border-radius: 1.6em;
		}

		.dark .pricing-box.best-price:not(#ace_mus) {
			background: #222;
			color: #bbb;
		}

		div#modern_list
			.pricing-box.best-price
			> .pricing-features
			.tleft.textOverflow {
			margin: 0;
		}

		div#modern_list
			.pricing-box.best-price
			> .pricing-features
			.tleft.textOverflow
			.icon-trophy {
			margin-right: 0.1em;
		}

		div#modern_list .pricing-box.best-price > .pricing-features span > small {
			font-size: 10pt;
		}

		div#modern_list .pricing-features > .bofu_meters,
		.pricing-action > span {
			padding: 0 2rem;
		}

		div#modern_list .pricing-features > .bofu_meters {
			display: -webkit-box;
			display: -ms-flexbox;
			display: flex;
			height: 7.5em;
			-ms-flex-wrap: wrap;
			flex-wrap: wrap;
			font-size: 1.2em;
			margin: 0;
		}

		div#modern_list .pricing-features > .bofu_meters > p {
			width: 100%;
			display: -webkit-box;
			display: -ms-flexbox;
			display: flex;
			-webkit-box-pack: start;
			-ms-flex-pack: start;
			justify-content: flex-start;
			line-height: 1;
		}

		div#modern_list .pricing-features > .bofu_meters > p > *:first-child {
			width: 5em;
			font-size: inherit;
		}

		div#modern_list .pricing-features > .bofu_meters > p > *:nth-child(3) {
			max-width: 7.2em;
			-webkit-box-flex: 1;
			-ms-flex: 1;
			flex: 1;
			margin-left: auto;
			font-size: inherit;
		}

		.textOverflow {
			margin: 0.2em 0.6em;
		}

		#ace_mus {
			background: -o-linear-gradient(
				30deg,
				rgba(131, 58, 180, 0.2) 0%,
				rgba(253, 29, 29, 0.2) 40%,
				rgba(252, 176, 69, 0.2) 100%
			);
			background: linear-gradient(
				60deg,
				rgba(131, 58, 180, 0.2) 0%,
				rgba(253, 29, 29, 0.2) 40%,
				rgba(252, 176, 69, 0.2) 100%
			);
		}

		.dark #ace_mus {
			color: #ddd;
		}

		.header_alpha {
			background: -o-linear-gradient(
				285deg,
				transparent 2.4em,
				#fffd 2.45em,
				#fff8 14em,
				transparent 14.05em
			);
			background: -o-linear-gradient(
				165deg,
				transparent 2.4em,
				#fffd 2.45em,
				#fff8 14em,
				transparent 14.05em
			);
			background: linear-gradient(
				285deg,
				transparent 2.4em,
				#fffd 2.45em,
				#fff8 14em,
				transparent 14.05em
			);
		}

		.header_alpha_dark {
			background: -o-linear-gradient(
				285deg,
				transparent 2.4em,
				#000e 2.45em,
				#0004 14em,
				transparent 14.05em
			);
			background: -o-linear-gradient(
				165deg,
				transparent 2.4em,
				#000e 2.45em,
				#0004 14em,
				transparent 14.05em
			);
			background: linear-gradient(
				285deg,
				transparent 2.4em,
				#000e 2.45em,
				#0004 14em,
				transparent 14.05em
			);
		}

		.pricing-box.best-price .header_alpha .pricing-title {
			text-shadow: none;
			-webkit-filter: drop-shadow(0 0 0.4em white) drop-shadow(0 0 0.8em white)
				drop-shadow(0 0 1.2em white);
			filter: drop-shadow(0 0 0.4em white) drop-shadow(0 0 0.8em white)
				drop-shadow(0 0 1.2em white);
		}

		.pricing-box.best-price .header_alpha_dark .pricing-title {
			text-shadow: none;
			-webkit-filter: drop-shadow(0 0 0.4em #2228) drop-shadow(0 0 0.8em #2228)
				drop-shadow(0 0 1.2em #2228);
			filter: drop-shadow(0 0 0.4em #2228) drop-shadow(0 0 0.8em #2228)
				drop-shadow(0 0 1.2em #2228);
		}

		.pricing-action {
			margin-top: 0.4em;
			padding: 0;
		}

		.pricing-action > span {
			display: -webkit-box;
			display: -ms-flexbox;
			display: flex;
			-webkit-box-pack: justify;
			-ms-flex-pack: justify;
			justify-content: space-between;
		}

		.pricing-action small {
			font-size: 1.2em;
		}

		div.TeamJumpArea {
			margin: 0.6em;
			-webkit-filter: drop-shadow(0 0 0.6em #0002);
			filter: drop-shadow(0 0 0.6em #0002);
		}

		[class^='icon-'] {
			font-family: 'font-icons' !important;
		}
	`;

	// add sorting dropdown
	sortLabel.innerHTML = '<i class="icon-sort-by-attributes"></i> Sort by';
	sortOptions.className = 'sm-form-control';
	sortOptions.onchange = (e) => {
		const { value } = e.target;
		sortEntries(value);
		localStorage.setItem('dee2formatter_sortby', value);
	};
	sortOptions.innerHTML = `
		<option value="fs">Final Strikers First</option>
		<option value="index">Entry No.</option>
		<option value="title">Title</option>
		<option value="artists">Artists</option>
		<option value="genre">Genre</option>
		<option value="updated">Latest Activity</option>
		<option value="impressions">Impressions</option>
		<option value="total">Total Points</option>
		<option value="median">Median Points</option>
		${entries[0].element.avg ? '<option value="avg">Average Points</option>' : ''}
		<option value="weighted">Weighted Score</option>
		<option value="random">Random</option>
	`;
	sortOptions.value = localStorage.getItem('dee2formatter_sortby') ?? 'random';
	document.querySelector('.TeamJumpArea').innerHTML = '';
	document.querySelector('.TeamJumpArea').appendChild(sortLabel);
	document.querySelector('.TeamJumpArea').appendChild(sortOptions);

	document
		.querySelector('#musiclist .content-wrap')
		.removeChild(
			document.querySelector('#musiclist .content-wrap>*:first-child')
		);
	for (let t of teams) {
		list.removeChild(t);
		// t.outerHTML = '';
	}
	list.appendChild(styles);
	for (let e of entries) {
		if (!isNaN(e.impressions)) {
			const total = e.element.stats.querySelector('p');
			const ranking = e.element.ranking;
			const rankingParent = ranking?.parentElement;
			const newImpressionElement = document.createElement('p');
			const weightedScoreElement = document.createElement('p');

			newImpressionElement.innerHTML = `<span>Impressions</span><b>${e.impressions}</b>`;
			total.innerHTML = `<span>Total</span><b>${e.total.toFixed(2)}</b>${
				e.fs
					? `<span>× 1.5 = <b>${(e.total * 1.5).toFixed(
							2
					  )}</b></span>`
					: ''
			}`;
			total.parentElement.insertBefore(newImpressionElement, total);
			if (rankingParent)
				rankingParent.innerHTML = `<span style="font-size:220%;font-family: 'Yanone Kaffeesatz', sans-serif; color:#FA8072;"></span>`;
			e.element.ranking = rankingParent?.firstElementChild;
			if (e.element.avg)
				e.element.avg.parentElement.innerHTML = `<span>Average</span><b>${e.avg.toFixed(
					2
				)}</b><meter value="${
					e.avg
				}" min="100" max="1000" low="500" high="850" optimum="1000"></meter>`;
			if (e.element.median)
				e.element.median.parentElement.innerHTML = `<span>Median</span><b>${e.median}</b><meter value="${e.median}" min="100" max="1000" low="700" high="950" optimum="1000"></meter>`;
			weightedScoreElement.innerHTML = `<span>Weighted</span><b>${e.weighted.toFixed(
				2
			)}</b><meter value="${e.weighted.toFixed(
				2
			)}" min="100" max="1000" low="600" high="900" optimum="1000"></meter>`;
			e.element.stats.appendChild(weightedScoreElement);

			e.element.total = total;
			e.element.avg = e.element.avg?.parentElement;
			e.element.median = e.element.median?.parentElement;
		} else {
			e.element.stats.innerHTML = '';
			e.element.total = null;
			e.element.avg = null;
			e.element.median = null;
		}

		e.element.base.removeAttribute('style');
		e.element.base.removeAttribute('class');
		e.element.title.title = e.title;
		e.element.artists.title = e.artists;
		e.element.genre.title = e.genre;
		e.element.artists.innerHTML = e.artists;
		e.element.info.parentElement.parentElement.removeAttribute('style');
		e.element.info.innerHTML = e.element.info.innerHTML.replace(
			/No\.\d+? \/ /,
			''
		);
		e.element.info.outerHTML = `<small>
			<i class="icon-music2"></i> ${e.production}
		</small><small>
			<i class="icon-clock"></i> ${e.updated.toLocaleString(undefined, {
				timeZone: 'JST',
			})}
		</small>`;
	}
	sortEntries(sortOptions.value);

	// .sort((a, b) => b.pts.avg - a.pts.avg)
	return entries;
})();