[PS] Homepage Enhancements

Improves your Pokemon Showdown homepage.

当前为 2024-09-03 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        [PS] Homepage Enhancements
// @namespace   https://greasyfork.org/en/users/1357767-indigeau
// @version     0.1
// @description Improves your Pokemon Showdown homepage.
// @match       https://play.pokemonshowdown.com/*
// @exclude     https://play.pokemonshowdown.com/sprites/*
// @author      indigeau
// @license     GNU GPLv3
// @icon        https://www.google.com/s2/favicons?sz=64&domain=pokemonshowdown.com
// @grant       none
// ==/UserScript==

// The gap between mainmenu elements
const GAP_MAIN = 20;
// The gap between .pm-window
const GAP_PM = 7;
// The gap between the edge of .pmbox and .pm-window
const GAP_PMBOX = 10;
// The height of #header
const HEIGHT_HEADER = 50;
// The height of .maintabbarbottom (height=6px + top or bottom border=1px)
const HEIGHT_STRIP = 7;
// The width of .mainmenu
const WIDTH_BATTLE = 270;
// The width of .pm-window
const WIDTH_PM = 270;

const styleSheet = (() => {
	const styleElement = document.createElement('style');
	
	document.head.appendChild(styleElement);
	
	return styleElement.sheet;
})();

// Style changes
(() => {
	for (const rule of [
		['body > :not(#room-.ps-room)', [['z-index', '1']]], // necessary to avoid blocking the header's pointer events
		['#room-.ps-room', [
			['top', '0'],
			['height', '100%'],
		]],
		['.mainmenuwrapper', [
			['display', 'flex'],
			['align-items', 'center'],
			['width', '100%'],
			['height', '100%'],
			['box-sizing', 'border-box'],
			['position', 'initial'],
		]],
		['.leftmenu', [
			['display', 'flex'],
			['flex-wrap', 'nowrap'],
			['flex-direction', 'row-reverse'],
			['width', '100%'],
			['height', '100%'],
			['box-sizing', 'border-box'],
			['padding', '0px'],
			['justify-content', 'center'],
			['padding', `${HEIGHT_HEADER + HEIGHT_STRIP + GAP_MAIN}px 0`],
			['position', 'absolute'],
			['top', '0'],
		]],
		['.activitymenu', [['display', 'contents']]],
		['.pmbox', [
			['flex-grow', '1'],
			['display', 'flex'],
			['flex-direction', 'column'],
			['height', '100%'],
			['flex-wrap', 'wrap'],
			['overflow', 'auto'],
			['background', 'rgba(0, 0, 0, .2)'],
			['border-radius', '20px'],
			['padding', GAP_PMBOX],
			['align-content', 'flex-start'],
			['overscroll-behavior', 'contain'],
			['margin-right', `${GAP_MAIN}px`],
			['box-sizing', 'border-box'],
			['align-items', 'center'],
		]],
		['.pmbox > *', [
			['width', `${WIDTH_PM}px`],
			['max-width', '100%'],
			['margin', GAP_PM],
		]],
		['.mainmenu', [
			['width', `${WIDTH_BATTLE}px`],
			['padding', '0'],
			['margin', `0 ${GAP_MAIN}px`],
			['height', '100%'],
			['display', 'flex'],
			['flex-direction', 'column'],
			['overflow-y', 'auto'],
			['scrollbar-width', 'none'],
			['overscroll-behavior', 'contain'],
			['background', 'rgba(0, 0, 0, .2)'],
			['border-radius', '20px'],
			['justify-content', 'space-evenly'],
		]],
		['.mainmenu > .menugroup', [
			['background', 'none'],
			['margin', '0'],
			['padding', '0'],
		]],
		['.mainmenu > .menugroup:not(:first-child) p', [['margin-top', '-1px']]],
		['.mainmenu > .menugroup:not(:first-child) p > button', [['box-shadow', 'inset #000d1733 0 0 200px 0px']]],
		['.dark > body .mainmenufooter', [['background', 'rgba(0, 0, 0, .3)']]],
		[':not(.dark) > body .mainmenufooter', [['background', 'url(../fx/client-topbar-bg.png) repeat-x left top scroll']]],
		['.mainmenufooter', [
			['height', `${HEIGHT_HEADER}px`],
			['padding-top', `${HEIGHT_STRIP - 1}px`],
			['width', '100%'],
			['bottom', '0'],
			['left', '0'],
			['display', 'flex'],
		]],
		['.mainmenufooter small', [
			['flex-grow', '1'],
			['display', 'flex'],
			['font-size', '0'],
			['border-top', '1px solid #34373b'],
		]],
		['.dark > body .mainmenufooter > small > a', [
			['color', '#fff'],
			['box-shadow', 'inset -0.5px -0.5px 1px 0.5px rgba(255, 255, 255, .5)'],
		]],
		['.dark > body .mainmenufooter > small > a:visited', [['color', '#fff']]],
		[':not(.dark) > body .mainmenufooter > small > a', [
			['color', '#222'],
			['box-shadow', 'inset -0.5px -0.5px 1px 0.5px rgba(255, 255, 255, .5)'],
		]],
		[':not(.dark) > body .mainmenufooter > small > a:visited', [['color', '#222']]],
		['.mainmenufooter > small > a', [
			['font-size', '12pt'],
			['text-align', 'center'],
			['text-decoration', 'none'],
			['border-top-right-radius', '0'],
			['border-top-left-radius', '0'],
			['margin', '0 5px'],
			['padding', '4px 12px'],
			['height', '28px'],
		]],
		['.mainmenufooter > small > a:hover', [
			['text-decoration', 'none'],
		]],
	]) {
		styleSheet.insertRule(`${rule[0]}{${rule[1].map(([property, value]) => `${property}:${value};`).join('')}}`);
	}
	
	for (const button of document.querySelectorAll('.mainmenufooter > small > a')) {
		button.classList.add('roomtab', 'button');
	}
})();

// Helper for hiding things when the screen's too narrow
class ConditionalDisplay {
	static source = document.querySelector('.mainmenuwrapper');
	static listeners = [];
	
	constructor(width) {
		this.width = width;
		
		this.isShown = this.doShow();
	}
	
	doShow() {
		return ConditionalDisplay.source.clientWidth >= this.width;
	}
	
	add(listener) {
		ConditionalDisplay.listeners.push(() => {
			if (this.doShow() === this.isShown) {
				return;
			}
			
			this.isShown = !this.isShown;
			
			listener(this.isShown);
		});
		
		listener(this.isShown);
	}
}

(new ResizeObserver(() => {
	for (const listener of ConditionalDisplay.listeners) {
		listener();
	}
})).observe(ConditionalDisplay.source);

// Footer extras
(() => {
	// Helpers
	
	const getButton = () => {
		const button = document.createElement('img');
		
		button.classList.add('icon', 'button');
		
		button.style.margin = '0 3px';
		button.style.height = '21px';
		button.style.borderRadius = '5px';
		button.style.cursor = 'pointer';
		button.style.padding = '2px';
		button.style.boxShadow = '.5px 1px 2px rgba(255, 255, 255, .45), inset .5px 1px 1px rgba(255, 255, 255, .5)';
		
		return button;
	};
	
	// Setup container
	
	const container = document.createElement('div');
	
	container.style.fontSize = '12pt';
	container.style.flexGrow = '1';
	container.style.margin = `0 ${GAP_MAIN}px`;
	container.style.alignItems = 'center';
	
	const showContainer = (doShow) => {
		container.style.display = doShow ? 'flex' : 'none';
	};
	
	(new ConditionalDisplay(850)).add(showContainer);
	
	// Setup text
	
	const text = document.createElement('span');
	
	text.style.marginLeft = '6px';
	
	text.innerText = 'Script by indigeau';
	
	// Setup pm button
	
	const psButton = getButton();
	
	psButton.src = 'https://www.google.com/s2/favicons?sz=64&domain=pokemonshowdown.com';
	
	psButton.addEventListener('click', () => {
		app.rooms[''].focusPM('indigeau');
	});
	
	// Setup feedback button
	
	const gfButton = getButton();
	
	gfButton.src = 'https://www.google.com/s2/favicons?sz=64&domain=greasyfork.org';
	
	gfButton.addEventListener('click', () => {
		open('https://greasyfork.org/en/scripts/506533-ps-homepage-enhancements/feedback');
	});
	
	// Setup seperator
	
	const tabBar = document.createElement('div');
	
	tabBar.classList.add('maintabbarbottom');
	
	tabBar.style.top = '0';
	
	// Add to DOM
	
	const footer = document.querySelector('.mainmenufooter');
	const parent = footer.lastElementChild;
	
	container.append(psButton, gfButton, text);
	
	parent.insertBefore(container, parent.firstChild);
	
	footer.append(tabBar);
})();

// Sidebar
(() => {
	// CSS for :hover style because it's easier than using listeners
	
	const IMAGE_BUTTON_CLASS = 'home-style-team-image';
	
	for (let rule of [
		[`.${IMAGE_BUTTON_CLASS}:hover + img`, [
			['scale', '1.2'],
			['filter', 'drop-shadow(black 3px 4px 2px) drop-shadow(black 0 0 10px)'],
			['z-index', '1'],
		]],
	]) {
		styleSheet.insertRule(`${rule[0]}{${rule[1].map(([property, value]) => `${property}:${value};`).join('')}}`);
	}
	
	// Helpers
	
	const swapImages = (toShow, toHide) => {
		for (const image of toHide) {
			image.style.display = 'none';
		}
		
		for (const image of toShow) {
			image.style.removeProperty('display');
		}
	};
	
	const update = (backupImages, teamImages, teamButtons, doListen = true) => {
		const button = document.querySelector('.select.teamselect');
		
		if (doListen) {
			(new MutationObserver(() => {
				update(backupImages, teamImages, teamButtons, false);
			})).observe(button.parentElement, {
				characterData: true,
				childList: true,
				subtree: true,
			});
		}
		
		const teamIndex = Number.parseInt(button.value);
		
		if (!Number.isInteger(teamIndex)) {
			swapImages(backupImages, teamImages);
			
			return;
		}
		
		const teamName = button.firstChild.innerText;
		const team = Storage.teams.find(({name}) => name === teamName);
		
		if (!team) {
			swapImages(backupImages, teamImages);
			
			return;
		}
		
		swapImages(teamImages, backupImages);
		
		for (const [i, mon] of Teams.unpack(team.team).entries()) {
			const {url, pixelated} = Dex.getSpriteData(mon.species, true, {...mon, ...team});
			
			teamImages[i].src = url;
			
			teamImages[i].style.imageRendering = pixelated ? 'pixelated' : 'auto';
			
			teamButtons[i].onclick = () => {
				app.addRoom('teambuilder');
				app.focusRoom('teambuilder');
				
				const {teambuilder} = app.rooms;
				
				teambuilder.edit(teamIndex);
				
				teambuilder.selectPokemon(i);
				
				teambuilder.stats();
				
				// You need to wait for this because it clears the stats
				if (teambuilder.formatResources[team.format] === true) {
					Object.defineProperty(teambuilder.formatResources, team.format, {
						// Avatar change listener
						set(value) {
							delete teambuilder.formatResources[team.format];
							
							teambuilder.formatResources[team.format] = value;
							
							window.setTimeout(() => {
								teambuilder.updateChart();
							}, 0);
						},
						get() {
							return true;
						},
					});
				}
			};
		}
	};
	
	const getTeamImage = (index) => {
		const image = document.createElement('img');
		
		image.style.position = 'absolute';
		image.style.top = `${(index + 1) / 7 * 100}%`;
		image.style.transform = 'translateY(-50%) scaleX(-1)';
		image.style.width = '100%';
		image.style.pointerEvents = 'none';
		image.style.transformOrigin = 'top';
		
		return image;
	};
	
	const getTeamButton = (index) => {
		const button = document.createElement('div');
		
		button.classList.add(IMAGE_BUTTON_CLASS);
		
		button.style.position = 'absolute';
		button.style.top = `${((index + 1) * 2 - 1) / 14 * 100}%`;
		button.style.width = '75%';
		button.style.height = `${1 / 7 * 100}%`;
		button.style.cursor = 'pointer';
		button.style.alignSelf = 'center';
		
		return button;
	};
	
	const getBackupImage = (src) => {
		const image = document.createElement('img');
		
		image.src = src;
		
		image.style.imageRendering = 'pixelated';
		image.style.transform = 'translateX(-2%) scaleX(-1)';
		
		return image;
	};
	
	// Setup container
	
	const container = document.createElement('div');
	
	container.style.height = '100%';
	container.style.background = 'rgba(0, 0, 0, .2)';
	container.style.alignItems = 'stretch';
	container.style.position = 'relative';
	container.style.overflow = 'hidden';
	container.style.flexDirection = 'column';
	container.style.marginLeft = `${GAP_MAIN}px`;
	container.style.borderRadius = '20px';
	container.style.maxWidth = '300px';
	container.style.width = '10%';
	container.style.placeContent = 'stretch space-around';
	container.style.flexDirection = 'column';
	container.style.justifyContent = 'center';
	
	const showContainer = (doShow) => {
		container.style.display = doShow ? 'flex' : 'none';
	};
	
	(new ConditionalDisplay((() => {
		// Minimum screen width when sidebar isn't visible
		const unconditionalWidth = GAP_MAIN * 3 + WIDTH_BATTLE + WIDTH_PM + GAP_PM * 2 + GAP_PMBOX * 2;
		
		// Sidebar width is 10% + GAP_MAIN; (100 / 0.9)% minimum width + GAP_MAIN must be available to accomodate it
		return Math.ceil(unconditionalWidth * (1 / 0.9)) + GAP_MAIN;
	})())).add(showContainer);
	
	// Setup mascots
	
	const backupImages = [];
	
	for (const src of [
		'https://play.pokemonshowdown.com/sprites/gen5ani/meloetta.gif',
		'https://play.pokemonshowdown.com/sprites/gen5ani/meloetta-pirouette.gif',
	]) {
		const image = getBackupImage(src);
		
		container.append(image);
		
		backupImages.push(image);
	}
	
	// Setup team members
	
	const teamImages = [];
	const teamButtons = [];
	
	for (let i = 0; i < 6; ++i) {
		const image = getTeamImage(i);
		const button = getTeamButton(i);
		
		container.append(button, image);
		
		teamImages.push(image);
		teamButtons.push(button);
	}
	
	// Initialise & setup team change listener
	
	update(backupImages, teamImages, teamButtons);
	
	// Add to DOM
	
	const parent = document.querySelector('.leftmenu');
	
	parent.append(container);
})();

/* global app Dex Teams */