您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Improves your Pokemon Showdown homepage.
当前为
// ==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 .leftmenu and its surrounding elements const GAP_MAIN = 20; // The gap between .pm-window elements const GAP_PM = 10; // 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 > #header', [['z-index', '1']]], ['#room-.ps-room', [ ['top', `${HEIGHT_HEADER}px`], ['height', `calc(100% - ${HEIGHT_HEADER + 2}px)`], ['scrollbar-width', 'none'], ]], ['.mainmenuwrapper', [ ['display', 'flex'], ['align-items', 'center'], ['width', '100%'], ['height', '100%'], ]], ['.leftmenu', [ ['display', 'flex'], ['flex-wrap', 'nowrap'], ['flex-direction', 'row-reverse'], ['width', '100%'], ['height', '100%'], ['box-sizing', 'border-box'], ['padding', '0px'], ['justify-content', 'center'], ['padding', `${GAP_MAIN + HEIGHT_STRIP - 1}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}px`], ['align-content', 'flex-start'], ['margin-right', `${GAP_MAIN}px`], ['box-sizing', 'border-box'], ['align-items', 'center'], ['scroll-snap-type', 'x mandatory'], ]], ['.pmbox > *', [ ['width', `${WIDTH_PM}px`], ['max-width', `calc(100% - ${GAP_PMBOX * 2}px)`], ['margin', `${GAP_PM}px`], ['scroll-snap-align', 'center'], ['scroll-margin-left', '20px'], ]], ['.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`], ['bottom', `-${HEIGHT_HEADER}px`], ['width', '100%'], ['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 = `-${HEIGHT_STRIP - 1}px`; // 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 */