您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Improves the Pokemon Showdown homepage.
当前为
// ==UserScript== // @name [PS] Homepage Enhancements // @namespace https://greasyfork.org/en/users/1357767-indigeau // @version 0.1 // @description Improves the 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== const main = () => { // 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%'], ['max-width', '100%'], // for when .ps-room.tiny-layout overrwrites width to auto ['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'], ['overflow', 'hidden'], ]], ['.ps-room.tiny-layout .pmbox > *', [ ['margin', `${GAP_PM}px`], ]], ['.pm-log', [ ['max-height', 'calc(100% - 22px) !important'], // Overwrites an element style. header=22px ['min-height', '0'], // Fixes an issue with the element failing to lose its scrollbar when it no longer needs it ['scrollbar-gutter', 'stable'], ['overflow-x', 'hidden'], ]], // Handle parent's scrollbar-gutter value ['.pm-log > *', [ ['width', 'calc(100% + 30px)'], ['box-sizing', 'border-box'], ['padding-right', '35px'], ]], ['.pm-log:has(+ .pm-log-add)', [ ['max-height', 'calc(100% - 53px) !important'], // Overwrites an element style. header=22px + .pm-log-add=31px ]], ['.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'], ['background', 'rgba(0, 0, 0, .2)'], ['border-radius', '20px'], ['justify-content', 'space-evenly'], ['flex-flow', 'wrap'], ]], ['.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'], ['text-shadow', 'rgb(255 255 255) 0px 0px 3px, rgb(255 255 255) 0px 0px 3px, rgb(255 255 255) 0px 0px 3px'], ['color', 'black'], ]], ['.mainmenufooter', [ ['height', `${HEIGHT_HEADER}px`], ['bottom', `-${HEIGHT_HEADER}px`], ['width', '100%'], ['left', '0'], ['display', 'flex'], ['flex-direction', 'row-reverse'], ]], ['.bgcredit.roomtab.button', [ ['overflow-y', 'hidden'], ['display', 'flex'], ['align-items', 'center'], ['max-width', '150px'], ]], ['.bgcredit.roomtab.button *', [ ['border', 'none'], ]], ['.bgcredit.roomtab.button a', [ ['padding', '0'], ['margin', '0'], ]], ['.bgcredit.roomtab.button > small', [ ['font-size', '12pt'], ['width', '100%'], ]], ['.bgcredit.roomtab.button small', [ ['display', 'block'], ['text-overflow', 'ellipsis'], ['contain', 'content'], ['white-space', 'pre'], ]], ['.bgcredit.roomtab.button:empty', [['display', 'none']]], ['.mainmenufooter > small', [ ['flex-grow', '1'], ['display', 'flex'], ['font-size', '0'], ['border-top', '1px solid #34373b'], ['justify-content', 'center'], ['max-width', '100%'], ['contain', 'size'], ]], ['.dark > body .mainmenufooter a, .dark > body .mainmenufooter a:visited', [['color', '#fff']]], ['.dark > body .mainmenufooter > small a, .dark > body .mainmenufooter > .bgcredit', [ ['box-shadow', 'inset 0.5px -0.5px 1px 0.5px rgba(255, 255, 255, .5)'], ]], [':not(.dark) > body .mainmenufooter a, :not(.dark) > body .mainmenufooter a:visited', [['color', '#222']]], [':not(.dark) > body .mainmenufooter > small a, :not(.dark) > body .mainmenufooter > .bgcredit', [ ['box-shadow', 'inset 0.5px -0.5px 1px 0.5px rgba(255, 255, 255, .5)'], ]], ['.mainmenufooter a, .bgcredit.roomtab.button', [ ['font-size', '12pt'], ['text-align', 'center'], ['text-decoration', 'none'], ['border-radius', '0'], ['margin', '0'], ['padding', '4px 12px'], ]], ['.mainmenufooter > small > a', [ ['height', '28px'], ]], ['.mainmenufooter a:hover', [['text-decoration', 'none']]], ['.mainmenufooter > small a:first-of-type', [['border-bottom-left-radius', '5px']]], ['.mainmenufooter > small a:last-of-type', [ ['border-bottom-right-radius', '5px'], ['overflow-x', 'hidden'], ['white-space', 'pre'], ['text-overflow', 'ellipsis'], ]], // tiny-layout ['.tiny-layout .mainmenufooter a', [['font-size', '10px']]], ['.tiny-layout .bgcredit.roomtab.button', [['display', 'none']]], ]) { styleSheet.insertRule(`${rule[0]}{${rule[1].map(([property, value]) => `${property}:${value};`).join('')}}`); } for (const button of [...document.querySelectorAll('.mainmenufooter > small > a'), ...document.querySelectorAll('.mainmenufooter > .bgcredit')]) { 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(870)).add(showContainer); // Setup text const textCredit = document.createElement('span'); textCredit.style.marginLeft = '6px'; textCredit.style.whiteSpace = 'pre'; textCredit.innerText = 'Script by '; const textName = document.createElement('span'); textName.style.color = '#258f14'; textName.style.fontWeight = 'bold'; textName.innerText = 'indigeau'; // Setup pm button const psButton = getButton(); psButton.src = 'https://www.google.com/s2/favicons?sz=64&domain=pokemonshowdown.com'; psButton.addEventListener('click', () => { window.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, textCredit, textName); 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 + *`, [ ['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) { const {data} = window.Storage.prefs; for (const property of ['bwgfx', 'noanim', 'nopastgens']) { let value = data[property] ?? false; Object.defineProperty(data, property, { set(_value) { value = _value; update(backupImages, teamImages, teamButtons, false); }, get() { return value; }, }); } (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, ...teamButtons]); return; } const teamName = button.firstChild.innerText; const team = window.Storage.teams.find(({name}) => name === teamName); if (!team) { swapImages(backupImages, [...teamImages, ...teamButtons]); return; } swapImages([...teamImages, ...teamButtons], backupImages); for (const [i, mon] of window.Teams.unpack(team.team).entries()) { const {url, pixelated, h, w} = window.Dex.getSpriteData(mon.species, true, {...mon, ...team, noScale: true}); const image = teamImages[i]; if (h > w) { image.style.scale = `${w / h}`; } else { image.style.removeProperty('scale'); } image.src = url; // pixelated just always looks better image.style.imageRendering = pixelated ? 'pixelated' : 'pixelated'; teamButtons[i].onclick = () => { window.app.addRoom('teambuilder'); window.app.focusRoom('teambuilder'); const {teambuilder} = window.app.rooms; teambuilder.edit(teamIndex); teambuilder.selectPokemon(i); teambuilder.stats(); // You need to listen for this assignment 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 container = document.createElement('div'); const image = document.createElement('img'); container.style.position = 'absolute'; container.style.top = `${(index + 1) / 7 * 100}%`; container.style.transform = 'translateY(-50%) scaleX(-1)'; container.style.width = '100%'; container.style.pointerEvents = 'none'; container.style.transformOrigin = 'top'; image.style.width = '100%'; container.append(image); return {container, 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.container); teamImages.push(image.image); teamButtons.push(button); } // Initialise & setup team change listener update(backupImages, teamImages, teamButtons); // Add to DOM const parent = document.querySelector('.leftmenu'); parent.append(container); })(); }; // Dealing with firefox being restrictive (() => { const context = typeof unsafeWindow === 'undefined' ? window : unsafeWindow; context._test = {}; const isAccessDenied = (() => { try { // Firefox throws `Error: Permission denied to access property "_"` which messes with the sidebar code // e.g. window.Dex.getSpriteData('', true, {}) throws a permission denied error when showdown reads options window.eval('_test._'); } catch (e) { return true; } return false; })(); delete context._test; if (isAccessDenied) { window.eval(`(${main.toString()})()`); } else { main(); } })();