// ==UserScript==
// @name [PS] Homepage Enhancements
// @namespace https://greasyfork.org/en/users/1357767-indigeau
// @version 0.14
// @description Improves the Pokémon 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 room = window.app.rooms[''];
const addResizeListener = (() => {
const TIMEOUT_DURATION = 20;
return (listener, target) => {
let isQueueing = false;
const callback = () => {
if (isQueueing) {
return;
}
isQueueing = true;
window.setTimeout(() => {
isQueueing = false;
listener();
}, TIMEOUT_DURATION);
};
if (target) {
(new ResizeObserver(callback)).observe(target);
} else {
window.addEventListener('resize', callback);
}
};
})();
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'],
['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'],
['scroll-snap-stop', 'always'],
]],
['.pmbox:has( .pm-notifying)', [
['background', 'rgba(37, 125, 123, 0.2)'],
]],
['.pmbox > *', [
['scroll-snap-align', 'start'],
['scroll-margin', `${GAP_PMBOX * 2}px`],
['width', `${WIDTH_PM}px`],
['max-width', `calc(100% - ${GAP_PMBOX * 2}px)`],
['margin', `${GAP_PM}px`],
['overflow', 'hidden'],
]],
['.pm-log', [
['max-height', `calc(100% - ${GAP_PMBOX * 2 + 2}px) !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 > p', [['margin-top', '-1px']]],
['.mainmenu > .menugroup > p > button:hover', [['box-shadow', 'inset rgba(0, 0, 0, 0.09) 0 0 0 12pt']]],
['.mainmenu > .menugroup > p > button:not(:hover)', [['box-shadow', 'inset rgba(0, 0, 0, 0.14) 0 0 0 12pt']]],
['.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');
}
})();
// Pmbox scroll handling
(() => {
const pmbox = room.$pmBox[0];
// Stick viewport to the left side when windows are added
let scrollLeft = pmbox.scrollLeft;
pmbox.addEventListener('scroll', () => {
scrollLeft = pmbox.scrollLeft;
});
(new MutationObserver(() => {
if (scrollLeft <= GAP_PMBOX * 2) {
pmbox.scrollLeft = scrollLeft;
}
})).observe(pmbox, {childList: true});
// Include challenge elements in vertical scrolling
for (const challenge of pmbox.querySelectorAll('.challenge')) {
const log = challenge.nextSibling;
log.insertBefore(challenge, log.firstChild);
}
const challengeGetter = window.MainMenuRoom.prototype.openChallenge;
window.MainMenuRoom.prototype.openChallenge = function (...args) {
const challenge = challengeGetter.call(this, ...args);
const log = challenge[0].nextSibling;
log.insertBefore(challenge[0], log.firstChild);
return challenge;
};
})();
// 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;
addResizeListener(() => {
const doShow = source.clientWidth >= width;
if (doShow !== isShown) {
isShown = doShow;
container.style.display = doShow ? 'flex' : 'none';
}
}, source);
container.style.display = isShown ? 'flex' : 'none';
})();
// 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', () => {
room.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_SIDEBAR = 'home-style-sidebar';
const CLASS_CONTAINER = 'home-style-container';
const CLASS_IMAGE = 'home-style-image';
const CLASS_BORDER_LEFT = 'home-style-border-l';
const CLASS_BORDER_RIGHT = 'home-style-border-r';
const CLASS_BACKGROUND = 'home-style-background';
const CLASS_WIDE = 'home-style-wide';
const CLASS_TALL = 'home-style-tall';
const CLASS_TINY = 'home-style-tiny';
const CLASS_SPLIT = 'home-style-col';
// The px width of the sidebar in landscape
const WIDTH_SIDEBAR = 100;
// The px thickness of image borders
const SIZE_BORDER = 8;
// The angle of slant in split view
const SKEW = 15;
for (let rule of [
// backgrounds
[`.${CLASS_BORDER_LEFT}, .${CLASS_BORDER_RIGHT}`, [
['border-color', 'rgb(0, 0, 0, 0.2)'],
['border-style', 'solid'],
['position', 'absolute'],
['height', '100%'],
['box-sizing', 'border-box'],
]],
[`:not(.${CLASS_SPLIT}) > .${CLASS_CONTAINER} > .${CLASS_BORDER_LEFT},
:not(.${CLASS_SPLIT}) > .${CLASS_CONTAINER} > .${CLASS_BORDER_RIGHT}`, [
['width', '50%'],
]],
[`.${CLASS_BORDER_LEFT}, .${CLASS_CONTAINER}:nth-child(2n) > .${CLASS_BACKGROUND}`, [
['left', '0'],
]],
[`.${CLASS_BORDER_RIGHT}, .${CLASS_CONTAINER}:nth-child(2n + 1) > .${CLASS_BACKGROUND}`, [
['right', '0'],
]],
[`.${CLASS_CONTAINER}:first-child > .${CLASS_BORDER_LEFT}`, [
['border-top-left-radius', '20px'],
]],
[`:not(.${CLASS_SPLIT}) > .${CLASS_CONTAINER}:first-child > .${CLASS_BORDER_RIGHT},
.${CLASS_SPLIT} > .${CLASS_CONTAINER}:nth-child(2) > .${CLASS_BORDER_RIGHT}`, [
['border-top-right-radius', '20px'],
]],
[`:not(.${CLASS_SPLIT}) > .${CLASS_CONTAINER}:last-child > .${CLASS_BORDER_LEFT},
.${CLASS_SPLIT} > .${CLASS_CONTAINER}:nth-child(5) > .${CLASS_BORDER_LEFT}`, [
['border-bottom-left-radius', '20px'],
]],
[`.${CLASS_CONTAINER}:last-child > .${CLASS_BORDER_RIGHT}`, [
['border-bottom-right-radius', '20px'],
]],
[`.${CLASS_SPLIT} > .${CLASS_CONTAINER}:nth-child(2n + 1) > .${CLASS_BORDER_LEFT},
.${CLASS_SPLIT} > .${CLASS_CONTAINER}:nth-child(2n) > .${CLASS_BORDER_RIGHT},
.${CLASS_SPLIT} > .${CLASS_CONTAINER} > .${CLASS_IMAGE}`, [
['transform', `skewX(${-SKEW}deg)`],
]],
[`.${CLASS_SPLIT} > .${CLASS_CONTAINER} > .${CLASS_BORDER_LEFT}`, [
['border-width', `${SIZE_BORDER}px 0 ${SIZE_BORDER}px ${SIZE_BORDER}px`],
]],
[`.${CLASS_SPLIT} > .${CLASS_CONTAINER} > .${CLASS_BORDER_RIGHT}`, [
['border-width', `${SIZE_BORDER}px ${SIZE_BORDER}px ${SIZE_BORDER}px 0`],
]],
[`:not(.${CLASS_SPLIT}) > .${CLASS_CONTAINER} > .${CLASS_BORDER_LEFT}`, [
['border-width', `${SIZE_BORDER}px 0 ${SIZE_BORDER}px ${SIZE_BORDER}px`],
]],
[`:not(.${CLASS_SPLIT}) > .${CLASS_CONTAINER} > .${CLASS_BORDER_RIGHT}`, [
['border-width', `${SIZE_BORDER}px ${SIZE_BORDER}px ${SIZE_BORDER}px 0`],
]],
[`.${CLASS_SPLIT} > .${CLASS_CONTAINER}:nth-child(2n + 1) > .${CLASS_BORDER_LEFT}`, [
['width', `calc(${100 - SKEW}% - ${SIZE_BORDER}px)`],
]],
[`.${CLASS_SPLIT} > .${CLASS_CONTAINER}:nth-child(2n) > .${CLASS_BORDER_RIGHT}`, [
['width', `calc(${100 + SKEW}% + ${SIZE_BORDER}px)`],
]],
[`.${CLASS_SPLIT} > .${CLASS_CONTAINER}:nth-child(2n + 1) > .${CLASS_BORDER_RIGHT},
.${CLASS_SPLIT} > .${CLASS_CONTAINER}:nth-child(2n) > .${CLASS_BORDER_LEFT}`, [
['width', '0'],
['height', `calc(100% - ${SIZE_BORDER * 2 - 0.3}px)`],
['top', `${SIZE_BORDER}px`],
]],
[`.${CLASS_SPLIT} > .${CLASS_CONTAINER} > .${CLASS_IMAGE}`, [
['padding', `${SIZE_BORDER + 4}px`],
]],
[`.${CLASS_SPLIT} > .${CLASS_CONTAINER}:nth-child(2n + 1) > .${CLASS_IMAGE}`, [
['padding-right', `calc(${SKEW}% - ${SIZE_BORDER * (100 - SKEW) / 100}px)`],
]],
[`.${CLASS_SPLIT} > .${CLASS_CONTAINER}:nth-child(2n) > .${CLASS_IMAGE}`, [
['padding-left', `calc(${SKEW}% - ${SIZE_BORDER * (100 - SKEW) / 100}px)`],
]],
[`:not(.${CLASS_SPLIT}) > .${CLASS_CONTAINER} > .${CLASS_IMAGE}`, [
['padding', `${SIZE_BORDER}px`],
]],
[`#${ID_SIDEBAR}`, [
['overflow', 'hidden'],
['margin-left', `${GAP_MAIN}px`],
['border-radius', '20px'],
['display', 'grid'],
]],
[`#${ID_SIDEBAR}:not(.${CLASS_SPLIT})`, [
['grid-auto-rows', `${100 / 6}%`],
['grid-auto-columns', '100%'],
]],
[`#${ID_SIDEBAR}.${CLASS_SPLIT}`, [
['grid-auto-rows', `${100 / 3}%`],
['grid-auto-columns', '50%'],
]],
[`#${ID_SIDEBAR}.${CLASS_SPLIT} > .${CLASS_CONTAINER}:nth-child(2)`, [
['grid-column', '2'],
]],
[`.${CLASS_CONTAINER}`, [
['cursor', 'pointer'],
['color', 'rgb(0, 0, 0, 0.2)'],
['position', 'relative'],
]],
[`.${CLASS_CONTAINER} > :not(.${CLASS_BACKGROUND})`, [
['pointer-events', 'none'],
]],
[`.${CLASS_BACKGROUND}`, [
['position', 'absolute'],
['width', `calc(100% + ${Math.tan(Math.PI * SKEW / 180) * 16}vh)`],
['height', '100%'],
['background-color', 'currentcolor'],
]],
[`.${CLASS_SPLIT} > .${CLASS_CONTAINER}`, [
['transform', `skewX(${SKEW}deg)`],
]],
[`.${CLASS_IMAGE}`, [
['height', '100%'],
['width', '100%'],
['box-sizing', 'border-box'],
['filter', 'drop-shadow(0 0 1px black)'],
['object-fit', 'contain'],
['image-rendering', 'pixelated'],
['pointer-events', 'none'],
]],
[`.${CLASS_CONTAINER}:hover > .${CLASS_BACKGROUND}`, [
['filter', 'brightness(1.15)'],
]],
[`.${CLASS_CONTAINER}:hover > .${CLASS_IMAGE}`, [
['filter', 'drop-shadow(0px 0px 3px black)'],
]],
// landscape
[`:not(.${CLASS_TALL}) > #${ID_SIDEBAR}`, [
['position', 'relative'],
['width', 'calc(50% - 280px)'],
['max-width', `${WIDTH_BATTLE}px`],
['min-width', `${WIDTH_SIDEBAR}px`],
]],
// portrait wide
[`.${CLASS_TALL} > #${ID_SIDEBAR}`, [
['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_SIDEBAR}`, [
['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('')}}`);
}
// Setup elements
const sidebar = document.createElement('div');
sidebar.id = ID_SIDEBAR;
const canvases = [];
for (let i = 0; i < 6; ++i) {
const container = document.createElement('div');
const background = document.createElement('div');
const backgroundL = document.createElement('div');
const backgroundR = document.createElement('div');
const canvas = document.createElement('canvas');
container.classList.add(CLASS_CONTAINER);
background.classList.add(CLASS_BACKGROUND);
backgroundL.classList.add(CLASS_BORDER_LEFT);
backgroundR.classList.add(CLASS_BORDER_RIGHT);
canvas.classList.add(CLASS_IMAGE);
background.draggable = true;
container.append(background, backgroundL, backgroundR, canvas);
sidebar.append(container);
canvases.push(canvas);
}
// Helpers
const updateRatio = (() => {
let ratio;
return (newRatio) => {
if (newRatio !== undefined) {
ratio = newRatio;
}
const singleRatio = (sidebar.clientWidth - SIZE_BORDER * 2) / ((sidebar.clientHeight - SIZE_BORDER * 12) / 6);
const doubleRatio = ((sidebar.clientWidth - SIZE_BORDER * 2) / 2) / ((sidebar.clientHeight - SIZE_BORDER * 6) / 2);
sidebar.classList[ratio / doubleRatio < singleRatio / ratio ? 'add' : 'remove'](CLASS_SPLIT);
};
})();
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');
} else {
const {teambuilder} = window.app.rooms;
while (teambuilder.curTeam) {
teambuilder.back();
}
}
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 = (canvases, empties) => {
for (const i of empties) {
const canvas = canvases[i];
canvas.parentElement.style.removeProperty('color');
canvas.parentElement.onclick = null;
canvas.parentElement.firstChild.ondragstart = null;
canvas.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;
};
const getColour = (() => {
const [getHash, getRGB] = (() => {
const bits = 3;
const shifts = 8 - bits;
const correction = 1 << (bits - 1);
return [
(r, g, b) => (r >> shifts) + ((g >> shifts) << bits) + ((b >> shifts) << (bits * 2)),
(hash) => ([
((hash % (1 << bits)) << shifts) + correction,
(((hash >> bits) % (1 << bits)) << shifts) + correction,
(((hash >> (bits * 2)) % (1 << bits)) << shifts) + correction,
]),
];
})();
const getColourCounts = (data) => {
const counts = new Map();
for (let i = 3; i < data.length; i += 4) {
if (data[i] === 0) {
continue;
}
const hash = getHash(data[i - 3], data[i - 2], data[i - 1]);
counts.set(hash, counts.has(hash) ? counts.get(hash) + 1 : 1);
}
return counts;
};
return ({data}) => getRGB(getColourCounts(data).entries().reduce(
(best, current) => best[1] > current[1] ? best : current,
[null, -1],
)[0]);
})();
const getData = (canvas, ctx, filter = 'saturate(1.5) blur(3px) saturate(1.5) blur(3px) saturate(1.5)', crop = 5, sections = 3, expand = 5) => {
const canvasMain = new OffscreenCanvas(canvas.width, canvas.height);
const ctxMain = canvasMain.getContext('2d', {alpha: true});
ctxMain.drawImage(canvas, 0, 0, canvas.width, canvas.height, crop, crop, canvas.width - crop * 2, canvas.height - crop * 2);
ctxMain.globalCompositeOperation = 'source-atop';
ctxMain.drawImage(canvas, 0, 0, canvas.width, canvas.height, -crop, -crop, canvas.width + crop * 2, canvas.height + crop * 2);
ctx.filter = 'brightness(0)';
ctx.globalCompositeOperation = 'copy';
ctx.drawImage(canvasMain, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'source-atop';
ctx.filter = filter;
const sectionHeight = canvas.height / sections;
const sectionWidth = canvas.width / sections;
for (let i = 0; i < sections; ++i) {
ctx.drawImage(canvasMain, 0, sectionHeight * i, canvas.width, sectionHeight, 0, sectionHeight * i - expand, canvas.width, sectionHeight + expand * 2);
}
for (let i = 0; i < sections; ++i) {
ctx.drawImage(canvasMain, sectionWidth * i, 0, sectionWidth, canvas.height, sectionWidth * i - expand, 0, sectionWidth + expand * 2, canvas.height);
}
ctx.globalCompositeOperation = 'copy';
ctx.filter = 'none';
return ctx.getImageData(crop, crop, canvas.width - crop * 2, canvas.height - crop * 2);
};
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;
ctx.drawImage(image, left, top, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
canvas.parentElement.style.color = `rgb(${getColour(getData(canvas, ctx)).join(', ')})`;
ctx.drawImage(image, left, top, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
canvas.style.removeProperty('display');
return image;
};
})();
return (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(false);
},
get() {
return value;
},
});
}
(new MutationObserver(() => {
update(false);
})).observe(button.parentElement, {
characterData: true,
childList: true,
subtree: true,
});
}
const empties = [0, 1, 2, 3, 4, 5];
const teamIndex = Number.parseInt(button.value);
const promises = [];
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 = canvases[i];
canvas.style.removeProperty('display');
canvas.parentElement.onclick = onClick.bind(null, team, teamIndex, mon, i);
promises.push(setImage(canvas, url).then((image) => {
canvas.parentElement.firstChild.ondragstart = getOnDrag(image, canvas);
return canvas.width / canvas.height;
}));
}
}
}
fillEmpties(canvases, empties);
Promise.all(promises).then((ratios) => {
updateRatio(ratios.length === 0 ? 1 : (ratios.reduce((total, next) => total + next, 0) / ratios.length));
});
};
})();
// Drag & drop into pmwindows
(() => {
const pmbox = room.$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 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
});
})();
// Initialise & setup team change listener
update();
// Setup responsive layout
(() => {
const sizes = {
portrait: 783,
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.landscapeWidest = (WIDTH_BATTLE + 280) / 0.5; // ['width', 'calc(50% - 280px)']
const source = document.querySelector('.leftmenu');
const resizeLeftRight = () => {
if (source.classList.contains(CLASS_TALL) || window.innerWidth - window.Room.prototype.bestWidth < sizes.landscapeWide) {
room.bestWidth = sizes.portraitWide;
} else {
room.bestWidth = Math.min(sizes.landscapeWidest, window.innerWidth - window.Room.prototype.bestWidth);
}
// prevents `this.curRoom.show(, leftMin)` from affecting width
room.tinyWidth = room.bestWidth;
window.app.updateLayout();
};
addResizeListener(resizeLeftRight);
resizeLeftRight();
addResizeListener(() => {
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.clientWidth >= sizes.landscapeWidest || (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);
updateRatio();
resizeLeftRight();
return;
}
// portrait
source.classList.add(CLASS_TALL);
source.classList.remove(CLASS_TINY);
source.classList[source.clientWidth >= sizes.portraitWide ? 'add' : 'remove'](CLASS_WIDE);
room.bestWidth = sizes.portraitWide;
room.tinyWidth = sizes.portraitWide;
window.app.updateLayout();
updateRatio();
}, source);
})();
// Add to DOM
const parent = document.querySelector('.leftmenu');
parent.append(sidebar);
})();
};
// 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();
}
})();