您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Improves the Pokemon Showdown homepage.
当前为
// ==UserScript== // @name [PS] Homepage Enhancements // @namespace https://greasyfork.org/en/users/1357767-indigeau // @version 0.4 // @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 [ // Header ['body > #header', [['z-index', '1']]], // Content ['#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'], ['position', 'absolute'], ['top', '0'], ['padding', `${GAP_MAIN + HEIGHT_STRIP - 1}px 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'], ]], ['.pm-log', [ ['max-height', 'calc(100% - 22px) !important'], // Overwrites an element style. header=22px ['min-height', '0'], ['width', '100%'], ['box-sizing', 'border-box'], // 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-window > .pm-log > *', [ // needs extra specificity to override padding on .pm-log .inner ['width', 'calc(100% + 30px)'], ['box-sizing', 'border-box'], ['padding-right', '35px'], ]], ['.pm-log:has(+ .pm-log-add)', [ ['max-height', 'calc(100% - 52px) !important'], // Overwrites an element style. header=22px + .pm-log-add=30px ]], ['.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'], // Prevent wrapping ['width', '100%'], ['max-width', 'unset'], ]], ['.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'], ]], // Footer ['.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']]], ['.ps-room.tiny-layout .pmbox > *', [ ['margin', `${GAP_PM}px`], ]], ['.tiny-layout .leftmenu', [ ['width', '100%'], ['max-width', '100%'], ['padding', `${GAP_MAIN + HEIGHT_STRIP - 1}px 0`], ]], ]) { 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'); } })(); // 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.display = 'flex'; container.style.alignItems = 'center'; (() => { const source = document.querySelector('.leftmenu'); const width = 870; let isShown = source.clientWidth >= width; (new ResizeObserver(() => { const doShow = source.clientWidth >= width; if (doShow !== isShown) { isShown = doShow; container.style.display = doShow ? 'flex' : 'none'; } })).observe(source); })(); // 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 (() => { const ID_CONTAINER = 'home-style-sidebar'; const CLASS_IMAGE_CONTAINER = 'home-style-team-container'; const CLASS_IMAGE = 'home-style-image'; const CLASS_WIDE = 'home-style-wide'; const CLASS_TALL = 'home-style-tall'; const CLASS_TINY = 'home-style-tiny'; // The px width of the sidebar in landscape const WIDTH_SIDEBAR = 100; // The px thickness of image borders const SIZE_BORDER = 10; for (let rule of [ [`#${ID_CONTAINER}`, [ ['overflow', 'hidden'], ['margin-left', `${GAP_MAIN}px`], ['border-radius', '20px'], ]], [`.${CLASS_IMAGE_CONTAINER}`, [ ['border-radius', '20px'], ['cursor', 'pointer'], ['position', 'absolute'], ['transition', 'transform 0.2s cubic-bezier(0, 0, 0.3, 1)'], ['width', `calc(100% - ${SIZE_BORDER * 2}px)`], ['height', `${200 / 7}%`], ['text-align', 'center'], ['align-content', 'center'], ['border', `${SIZE_BORDER}px solid rgb(0, 0, 0, 0.2)`], ['color', 'rgb(0, 0, 0, 0.2)'], ['background-color', 'currentcolor'], ]], ...[0, 1, 2, 3, 4, 5].map((row) => [`.${CLASS_IMAGE_CONTAINER}:nth-of-type(${row + 1})`, [ ['top', `calc(${row / 7 * 100}% - ${row / 5 * SIZE_BORDER * 2}px)`], ]]), [`.${CLASS_IMAGE_CONTAINER}:hover ~ .${CLASS_IMAGE_CONTAINER}`, [ ['transform-origin', 'top'], ['transform', `translateY(calc(50% - ${20 - SIZE_BORDER}px))`], ]], [`.${CLASS_IMAGE}`, [ ['height', '100%'], ['padding', '1vh 1vw'], ['box-sizing', 'border-box'], ['filter', 'drop-shadow(0 0 1px black)'], ]], [`.${CLASS_IMAGE}`, [ ['width', '100%'], ['object-fit', 'contain'], ['image-rendering', 'pixelated'], ['pointer-events', 'none'], ]], // landscape [`:not(.${CLASS_TALL}) > #${ID_CONTAINER}`, [ ['position', 'relative'], ['width', 'calc(50% - 280px)'], ['max-width', `${WIDTH_BATTLE}px`], ['min-width', `${WIDTH_SIDEBAR}px`], ]], // portrait wide [`.${CLASS_TALL} > #${ID_CONTAINER}`, [ ['position', 'absolute'], ['bottom', `${GAP_MAIN + HEIGHT_STRIP - 1}px`], ['left', '0'], ['height', `calc(50% - ${GAP_MAIN + HEIGHT_STRIP - 1}px)`], ['width', `${WIDTH_BATTLE}px`], ]], [`.${CLASS_TALL} > .mainmenu`, [ ['height', `calc(50% - ${GAP_MAIN}px)`], ]], // thin [`:not(.${CLASS_WIDE}) > #${ID_CONTAINER}`, [ ['display', 'none'], ]], [`.${CLASS_TALL}:not(.${CLASS_WIDE}) > .mainmenu, .${CLASS_TINY} > .mainmenu`, [ ['width', `calc(100% - ${GAP_MAIN * 2}px)`], ]], [`.${CLASS_TALL}:not(.${CLASS_WIDE}) > .activitymenu > .pmbox`, [ ['position', 'absolute'], ['bottom', `${GAP_MAIN}px`], ['left', `${GAP_MAIN}px`], ['height', `calc(50% - ${GAP_MAIN}px)`], ['width', `calc(100% - ${GAP_MAIN * 2}px)`], ]], // tiny [`.${CLASS_TINY} > .activitymenu > .pmbox`, [ ['display', 'none'], ]], ]) { styleSheet.insertRule(`${rule[0]}{${rule[1].map(([property, value]) => `${property}:${value};`).join('')}}`); } // Helpers const update = (() => { const styleSheet = (() => { const styleElement = document.createElement('style'); document.head.appendChild(styleElement); return styleElement.sheet; })(); const onClick = (team, teamIndex, {species}, monIndex, {ctrlKey}) => { let roomWasClosed = !window.app.rooms['teambuilder']; if (roomWasClosed) { window.app.addRoom('teambuilder'); } const {teambuilder} = window.app.rooms; teambuilder.edit(teamIndex); teambuilder.selectPokemon(monIndex); if (ctrlKey) { window.open(teambuilder.smogdexLink(species)); if (roomWasClosed) { window.app.removeRoom('teambuilder'); } return; } window.app.focusRoom('teambuilder'); 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 getOnDrag = (image, canvas) => { const raw = { url: image.src, image, }; const cropped = { url: canvas.toDataURL(), image: new Image(), }; raw.image.src = raw.url; cropped.image.src = cropped.url; return ({dataTransfer, ctrlKey}) => { const {url, image} = ctrlKey ? cropped : raw; dataTransfer.setData('text/plain', url); dataTransfer.setDragImage(image, image.width / 2, image.height / 2); }; }; const fillEmpties = (images, empties) => { for (const i of empties) { const image = images[i]; image.parentElement.style.removeProperty('color'); image.parentElement.onclick = null; image.style.display = 'none'; } }; const setImage = (() => { const getLeft = (data, width) => { for (let i = 3; i < data.length; i += 4) { for (let j = i; j < data.length; j += (width * 4)) { if (data[j] > 0) { return (i - 3) / 4; } } } return null; }; const getTop = (data, rowLength) => { for (let i = 3; i < data.length; i += 4) { if (data[i] > 0) { return Math.floor(i / rowLength); } } return null; }; const getRight = (data, rowLength) => { for (let i = data.length - 1; i >= 3; i -= 4) { for (let j = i; j >= 3; j -= rowLength) { if (data[j] > 0) { return (rowLength - data.length + i + 1) / 4; } } } return null; }; const getBottom = (data, rowLength) => { for (let i = data.length - 1; i >= 3; i -= 4) { if (data[i] > 0) { return Math.floor(i / rowLength) + 1; } } return null; }; return async (canvas, src) => { const ctx = canvas.getContext('2d', {willReadFrequently: true, alpha: true}); const image = new Image(); await new Promise((resolve) => { image.onload = resolve; image.src = src; }); canvas.width = image.width; canvas.height = image.height; ctx.drawImage(image, 0, 0); const {data, height, width} = ctx.getImageData(0, 0, canvas.width, canvas.height); const rowLength = width * 4; const left = getLeft(data, rowLength) ?? 0; const top = getTop(data, rowLength) ?? 0; const right = getRight(data, rowLength) ?? width; const bottom = getBottom(data, rowLength) ?? height; canvas.width = right - left; canvas.height = bottom - top; // Blur lines & focus main colours ctx.filter = 'saturate(2) blur(10px) saturate(3) saturate(0.6)'; ctx.drawImage(image, left, top, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); canvas.parentElement.style.color = `rgba(${(new window.ColorThief()).getColor(canvas).join(', ')}, 0.8)`; ctx.reset(); ctx.drawImage(image, left, top, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); return image; }; })(); return (container, teamImages, 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(container, teamImages, false); }, get() { return value; }, }); } (new MutationObserver(() => { update(container, teamImages, false); })).observe(button.parentElement, { characterData: true, childList: true, subtree: true, }); } const empties = [0, 1, 2, 3, 4, 5]; const teamIndex = Number.parseInt(button.value); for (let i = styleSheet.cssRules.length - 1; i >= 0; --i) { styleSheet.deleteRule(i); } if (Number.isInteger(teamIndex)) { const teamName = button.firstChild.innerText; const teamText = window.Storage.teams.find(({name}) => name === teamName); if (teamText) { const team = window.Teams.unpack(teamText.team); for (let i = team.length - 1; i >= 0; --i) { const mon = team[i]; if (!mon.species) { continue; } empties.splice(i, 1); const {url} = window.Dex.getSpriteData(mon.species, true, {...mon, ...teamText, noScale: true}); const canvas = teamImages[i]; canvas.style.removeProperty('display'); teamImages[i].parentElement.onclick = onClick.bind(null, team, teamIndex, mon, i); setImage(canvas, url).then((image) => { teamImages[i].parentElement.ondragstart = getOnDrag(image, canvas); }); } } } fillEmpties(teamImages, empties); }; })(); const getTeamImage = () => { const container = document.createElement('div'); container.classList.add(CLASS_IMAGE_CONTAINER); container.draggable = true; const image = document.createElement('canvas'); image.classList.add(CLASS_IMAGE); container.append(image); return image; }; // Drag & drop into pmwindows (() => { const pmbox = window.app.rooms[''].$pmBox[0]; pmbox.addEventListener('drop', (event) => { event.stopPropagation(); const isInWindow = (id) => event.target.matches(`.pm-window-${id} *`) || event.target.classList.contains(`.pm-window-${id}`); if (event.target.matches('textarea')) { window.setTimeout(() => { event.target.dispatchEvent(new KeyboardEvent('keyup')); // force a resize }, 0); return; } let name; if (event.target.isSameNode(pmbox) || isInWindow('')) { name = '~'; } else if (isInWindow(window.app.user.get('userid'))) { name = window.app.user.get('name'); } else { return; } const room = window.app.rooms['']; const message = `/raw <img src="${event.dataTransfer.getData('text/plain')}" class="pixelated" style="vertical-align: middle;" />`; room.addPM(name, message, name); room.openPM(name).find('textarea[name=message]').focus(); // remove .pm-notifying }); })(); // Setup container const container = document.createElement('div'); container.id = ID_CONTAINER; // Setup dynamic sizing classes (() => { const sizes = { portrait: 900, portraitSmall: 450, }; sizes.portraitWide = GAP_MAIN * 3 + WIDTH_BATTLE + WIDTH_PM + GAP_PM * 2 + GAP_PMBOX * 2; sizes.landscapeWide = sizes.portraitWide + WIDTH_SIDEBAR + GAP_MAIN; sizes.portraitWide = Math.min(window.MainMenuRoom.prototype.bestWidth - 1, sizes.portraitWide); const source = document.querySelector('.leftmenu'); (new ResizeObserver(() => { if (source.clientHeight < sizes.portraitSmall && source.clientWidth < sizes.portraitWide) { // tiny source.classList.add(CLASS_TINY); source.classList.remove(CLASS_TALL); source.classList.remove(CLASS_WIDE); return; } if (source.clientHeight < sizes.portraitSmall || (source.clientHeight < sizes.portrait && source.clientWidth > sizes.portraitWide)) { // landscape source.classList.remove(CLASS_TALL); source.classList.remove(CLASS_TINY); source.classList[source.clientWidth > sizes.landscapeWide ? 'add' : 'remove'](CLASS_WIDE); return; } // portrait source.classList.add(CLASS_TALL); source.classList.remove(CLASS_TINY); source.classList[source.clientWidth > sizes.portraitWide ? 'add' : 'remove'](CLASS_WIDE); })).observe(source); })(); // Setup team members const teamImages = []; for (let i = 0; i < 6; ++i) { const image = getTeamImage(); container.append(image.parentElement); teamImages.push(image); } // Initialise & setup team change listener update(container, teamImages); // 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(); } })();