// ==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==
const styleSheet = (() => {
const styleElement = document.createElement('style');
document.head.appendChild(styleElement);
return styleElement.sheet;
})();
// Style
(() => {
const rules = [
['body > :not(#room-.ps-room)', [['z-index', '1']]], // necessary to avoid blocking the header's pointer events
['#room-.ps-room', [
// ['padding-bottom', '50px'], // pushes the footer offscreen
['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-top', '77px'], // headerHeight + 20px
['padding-bottom', '70px'], // footerHeight + 20px
['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', '10px'],
['align-content', 'flex-start'],
['overscroll-behavior', 'contain'],
['margin-right', '22px'],
['box-sizing', 'border-box'],
['align-items', 'center'],
]],
['.pmbox > *', [
['width', '270px'],
['max-width', '100%'],
['margin', '4px'],
]],
['.mainmenu', [
['width', '270px'],
['padding', '0'],
['margin', '0 22px'],
['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)'],
['border-top', '6px solid #555'],
]],
[':not(.dark) > body .mainmenufooter', [
['background', 'url(../fx/client-topbar-bg.png) repeat-x left top scroll'],
['border-top', '6px solid #f8f8f8'],
]],
['.mainmenufooter', [
['height', '50px'],
['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'],
]],
];
for (let rule of rules) {
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');
}
})();
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);
// todo handle html:not(.dark) & html.dark differently
// footer bg 'url(../fx/client-topbar-bg.png) repeat-x left top scroll'
// Footer extras
(() => {
const parent = document.querySelector('.mainmenufooter > small');
const container = document.createElement('div');
container.style.fontSize = '12pt';
container.style.flexGrow = '1';
container.style.margin = '0 20px';
container.style.alignItems = 'center';
const showContainer = (doShow) => {
container.style.display = doShow ? 'flex' : 'none';
};
(new ConditionalDisplay(850)).add(showContainer);
const text = document.createElement('span');
text.style.marginLeft = '6px';
text.innerText = 'Script by indigeau';
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;
};
// PS button
const psButton = (() => {
const button = getButton();
button.src = 'https://www.google.com/s2/favicons?sz=64&domain=pokemonshowdown.com';
button.addEventListener('click', () => {
app.rooms[''].focusPM('indigeau');
});
return button;
})();
// GreasyFork button
const gfButton = (() => {
const button = getButton();
button.src = 'https://www.google.com/s2/favicons?sz=64&domain=greasyfork.org';
button.addEventListener('click', () => {
open('https://greasyfork.org/en/scripts/506533-ps-homepage-enhancements/feedback');
});
return button;
})();
container.append(psButton, gfButton, text);
parent.insertBefore(container, parent.firstChild);
})();
// Side mascots
(() => {
const imageButtonClass = 'home-style-team-image';
const rules = [
[`.${imageButtonClass}:hover + img`, [
['scale', '1.2'],
['filter', 'drop-shadow(black 3px 4px 2px) drop-shadow(black 0 0 10px)'],
['z-index', '1'],
]],
];
for (let rule of rules) {
styleSheet.insertRule(`${rule[0]}{${rule[1].map(([property, value]) => `${property}:${value};`).join('')}}`);
}
const IMAGE_BACKUP = [
{
src: 'https://play.pokemonshowdown.com/sprites/gen5ani/meloetta.gif',
transform: 'translateX(-2%) scaleX(-1)',
},
{
src: 'https://play.pokemonshowdown.com/sprites/gen5ani/meloetta-pirouette.gif',
transform: 'translateX(-2%) scaleX(-1)',
},
];
const swapImages = (toShow, toHide) => {
for (const image of toHide) {
image.style.display = 'none';
}
for (const image of toShow) {
image.style.removeProperty('display');
}
};
const setImageSrc = (backupImages, teamImages, teamButtons, doListen = true) => {
const button = document.querySelector('.select.teamselect');
if (doListen) {
(new MutationObserver(() => {
setImageSrc(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(imageButtonClass);
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, transform}) => {
const image = document.createElement('img');
image.src = src;
image.style.imageRendering = 'pixelated';
image.style.transform = transform;
return image;
};
const parent = document.querySelector('.leftmenu');
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 = '22px';
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 teamImages = [];
const teamButtons = [];
const backupImages = [];
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);
}
for (const data of IMAGE_BACKUP) {
const image = getBackupImage(data);
container.append(image);
backupImages.push(image);
}
setImageSrc(backupImages, teamImages, teamButtons);
const showContainer = (doShow) => {
container.style.display = doShow ? 'flex' : 'none';
};
(new ConditionalDisplay((() => {
const unconditionalWidth
// margins between elements
= 22 * 3
// mainmenu width
+ 270
// pm-window width + pm-window margin + pmbox padding
+ 270 + 4 * 2 + 10 * 2;
return Math.ceil(unconditionalWidth * (1 / 0.9)) + 22;
})())).add(showContainer);
parent.append(container);
})();
/* global app Dex Teams */