// ==UserScript==
// @namespace littleendu.xyz
// @match https://pxls.space/
// @match https://new.reddit.com/r/place/*
// @match https://www.reddit.com/r/place/*
// @match https://garlic-bread.reddit.com/embed*
// @match https://hot-potato.reddit.com/embed*
// @match https://www.twitch.tv/otknetwork/*
// @match https://9jjigdr1wlul7fbginbq7h76jg9h3s.ext-twitch.tv/*
// @match https://place.ludwig.gg/*
// @grant GM.xmlHttpRequest
// @grant GM.setValue
// @grant GM.getValue
// @connect *
// @name rplace amongus detector
// @version 0.7.1
// @description Manages your templates on various canvas games
// @author LittleEndu, Mikarific, April
// @license MIT
//
// Created with love using Gorilla
// ==/UserScript==
(function () {
'use strict';
const patterns_new = [
`
0111
1100
1111
0111
0101
`,
`
0111
1100
1111
1111
0101
`,
`
1111
1100
1111
0111
0101
`,
`
111
100
111
111
101
`,
`
111
100
111
101
101
`,
`
0111
1100
1111
0101
`,
`
0111
1100
0111
0101
`,
`
111
100
111
101
`,
`
011
100
111
101
`,
]
const genVariants = (pattern, { w, h }) => {
const patterns = [
pattern,
pattern.map(({ pos, ...fields }) => ({
pos: [w - pos[0], pos[1]],
...fields
})),
pattern.map(({ pos, ...fields }) => ({
pos: [pos[0], h - pos[1]],
...fields
})),
pattern.map(({ pos, ...fields }) => ({
pos: [w - pos[0], h - pos[1]],
...fields
}))
]
for (let pattern of patterns) {
if (pattern.length !== w * h)
console.log('pattern with wrong size', [w, h], w * h, pattern.length)
}
return patterns.map((pixels) => ({ pixels: pixels.sort(({ pos: a }, { pos: b }) => (b[0] * b[1]) - (a[0] * a[1])), w, h }))
}
const patterns = patterns_new
.map((p) => {
const wihtoutSpaces = p.trim().replaceAll(" ", "");
const width = wihtoutSpaces.indexOf("\n");
const height = wihtoutSpaces.split("\n").length;
return {
w: width,
h: height,
pattern: wihtoutSpaces
.replaceAll("\n", "")
.split("")
.reduce(
(curr, item, index) => [
...curr,
{
pos: [index % width, Math.floor(index / width)],
enabled: item === "1"
}
],
[]
)
};
})
.map(({ w, h, pattern }) => genVariants(pattern, { w, h }))
.flat()
const genAmongus = (canvas) => {
const start = Date.now();
const overlay = document.createElement('canvas');
overlay.height = canvas.height;
overlay.width = canvas.width;
const ctx = overlay.getContext("2d");
ctx.globalAlpha = 1;
ctx.imageSmoothingEnabled = false;
ctx.drawImage(canvas, 0, 0);
const orgPixels = ctx.getImageData(0, 0, overlay.width, overlay.height);
const pixels = new Uint32Array(orgPixels.data.buffer)
const pixelColor = (pixels, x, y) => {
return pixels[(x + y * overlay.width)];
};
const pixelEqual = (a, b) => a === b;
const foundPatterns = [];
const patternCheck = (patternPixels, canvaPixels, startX, startY) => {
const firstEnabled = patternPixels.find(({ enabled }) => enabled)
let refColor = pixelColor(
canvaPixels,
startX + firstEnabled.pos[0],
startY + firstEnabled.pos[1]
);
for (let index = 0; index < patternPixels.length; index++) {
const patternPixel = patternPixels[index];
const color = pixelColor(
canvaPixels,
startX + patternPixel.pos[0],
startY + patternPixel.pos[1]
);
if (patternPixel.enabled && !pixelEqual(color, refColor)) return false;
if (!patternPixel.enabled && pixelEqual(color, refColor)) return false;
}
return true;
};
const smallestPatternX = 4;
const smallestPatternY = 4;
for (let x = 0; x < overlay.width - smallestPatternX; x++) {
for (let y = 0; y < overlay.height - smallestPatternY; y++) {
for (let pattern of patterns) {
if (patternCheck(pattern.pixels, pixels, x, y)) {
foundPatterns.push({ pos: [x, y], pattern });
break;
}
}
}
}
ctx.fillStyle = "rgba(1,1,1, 0.7)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
foundPatterns.forEach(({ pos: start, pattern }) => {
pattern.pixels.forEach(({ pos, enabled }) => {
if (enabled) {
const p = [start[0] + pos[0], start[1] + pos[1]];
const color = pixelColor(pixels, p[0], p[1]);
const r = color & 0xff, g = (color & 0xff00)>>>8, b = (color & 0xff0000)>>>16;
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
ctx.fillRect(p[0], p[1], 1, 1);
}
});
});
const end = Date.now();
const took = end - start;
console.log("took", took);
console.log("found", foundPatterns.length);
return {
overlay,
foundPatterns,
took
}
}
const css = (x) => x;
const MAX_TEMPLATES = 100;
const CACHE_BUST_PERIOD = 1000 * 60 * 2;
const UPDATE_PERIOD_MILLIS = 100;
const TEMPLATE_RELOAD_INTERVAL = 1000 * 60 * 5;
const SECONDS_SPENT_BLINKING = 5;
const AMOUNT_OF_BLINKING = 11;
const ANIMATION_DEFAULT_PERCENTAGE = 1 / 3;
const NO_JSON_TEMPLATE_IN_PARAMS = "no_json_template";
const CONTACT_INFO_CSS = css `
div.iHasContactInfo {
max-width: 30px;
padding: 1px;
font-size: 1px; /* these 3 will be overwritten, but oh well */
width: max-content;
white-space: nowrap;
overflow: hidden;
font-weight: bold;
font-family: serif; /* this fixes firefox */
color: #eee;
background-color: #111;
opacity: 0;
transition: opacity 500ms, width 200ms, height 200ms, max-width 200ms;
position: absolute;
pointer-events: none;
z-index: 9999999;
}
div.iHasContactInfo:hover {
z-index: 99999999;
max-width: 100%;
width: auto;
}
`;
const GLOBAL_CSS = css `
#osuplaceNotificationContainer {
width: 200px;
height: 66%;
position: absolute;
z-index: 9999;
top: -0.1px;
right: 10px;
background-color: rgba(255, 255, 255, 0);
pointer-events: none;
user-select: none;
}
.osuplaceNotification {
border-radius: 8px;
background-color: #621;
color: #eee;
transition: height 300ms, opacity 300ms, padding 300ms, margin 300ms;
overflow: hidden;
pointer-events: auto;
cursor: pointer;
word-wrap: break-word;
height: 0px;
opacity: 0;
padding: 0px;
margin: 0px;
}
.osuplaceNotification.visible {
height: auto;
opacity: 1;
padding: 8px;
margin: 8px;
}
#settingsOverlay {
transition: opacity 300ms ease 0s;
width: 100vw;
height: 100vh;
position: absolute;
left: -0.1px;
top: -0.1px;
background-color: rgba(0, 0, 0, 0.75);
padding: 0px;
margin: 0px;
opacity: 0;
pointer-events: none;
z-index: 2147483647;
text-align: center;
user-select: none;
overflow-y: auto;
font-size: 14px;
}
`;
const SETTINGS_CSS = css `
label,
button{
height: auto;
white-space: normal;
word-break: break-word;
text-shadow: -1px -1px 1px #111, 1px 1px 1px #111, -1px 1px 1px #111, 1px -1px 1px #111;
color: #eee;
}
input {
width: auto;
max-width: 100%;
height: auto;
color: #eee;
background-color: #111;
-webkit-appearance: auto;
border-radius: 5px;
font-size: 14px;
}
.settingsWrapper {
background-color: rgba(0, 0, 0, 0.5);
padding: 8px;
border-radius: 8px;
border: 1px solid rgba(238, 238, 238, 0.5);
margin: 0.5rem auto 0.5rem auto;
min-width: 13rem;
max-width: 20%;
}
#templateLinksWrapper button{
word-break: break-all;
cursor: pointer;
}
.settingsWrapper:empty {
display: none;
}
.settingsButton {
cursor: pointer;
display: inline-block;
color: rgb(238, 238, 238);
background-color: rgba(0, 0, 0, 0.5);
padding: 0.25rem 0.5rem;
margin: 0.5rem;
border-radius: 5px;
line-height: 1.1em;
border: 1px solid rgba(238, 238, 238, 0.5);
}
.settingsButton:hover {
background-color: rgba(64, 64, 64, 0.5);
}
.settingsSliderBox, .settingsCheckbox {
background-color: rgba(0, 0, 0, 0.5);
padding: 0.25rem 0.5rem;
border-radius: 5px;
margin: 0.5rem;
}
.templateLink:hover {
background-color: rgba(128, 0, 0, 0.5);
}
`;
function run() {
let reticuleStyleSetter = setInterval(() => {
var _a, _b;
let embed = document.querySelector('garlic-bread-embed');
let preview = (_a = embed === null || embed === void 0 ? void 0 : embed.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('garlic-bread-pixel-preview');
if (preview) {
clearInterval(reticuleStyleSetter);
let style = document.createElement('style');
style.innerHTML = '.pixel { clip-path: polygon(-20% -20%, 120% -20%, 120% 20%, 63% 20%, 63% 37%, 37% 37%, 37% 20%, 20% 20%, 20% 37%, 37% 37%, 37% 63%, 20% 63%, 20% 80%, 37% 80%, 37% 63%, 63% 63%, 63% 80%, 80% 80%, 80% 63%, 63% 63%, 63% 37%, 80% 37%, 80% 20%, 120% 20%, 120% 120%, -20% 120%);}';
console.log(preview);
(_b = preview === null || preview === void 0 ? void 0 : preview.shadowRoot) === null || _b === void 0 ? void 0 : _b.appendChild(style);
}
}, UPDATE_PERIOD_MILLIS);
}
function negativeSafeModulo(a, b) {
return (a % b + b) % b;
}
function getFileStemFromUrl(url) {
const lastSlashIndex = url.lastIndexOf('/');
const fileName = url.slice(lastSlashIndex + 1);
const lastDotIndex = fileName.lastIndexOf('.');
const fileStem = (lastDotIndex === -1) ? fileName : fileName.slice(0, lastDotIndex);
return fileStem;
}
function windowIsEmbedded() {
return window.top !== window.self;
}
async function sleep(ms) {
await new Promise(resolve => setTimeout(resolve, ms));
}
function stringToHtml(str) {
let div = document.createElement('div');
div.innerHTML = str;
return div.firstChild;
}
function wrapInHtml(html, str) {
let tag = document.createElement(html);
tag.innerText = str;
return tag;
}
function removeItem(array, item) {
let index = array.indexOf(item);
if (index !== -1) {
array.splice(index, 1);
}
}
function findJSONTemplateInParams(urlString) {
const urlSearchParams = new URLSearchParams(urlString);
const params = Object.fromEntries(urlSearchParams.entries());
console.log(params);
return params.jsontemplate ? params.jsontemplate : null;
}
function findJSONTemplateInURL(url) {
return findJSONTemplateInParams(url.hash.substring(1)) || findJSONTemplateInParams(url.search.substring(1));
}
function findElementOfType(element, type) {
let rv = [];
if (element instanceof type) {
console.log('found canvas', element, window.location.href);
rv.push(element);
}
// find in Shadow DOM elements
if (element instanceof HTMLElement && element.shadowRoot) {
rv.push(...findElementOfType(element.shadowRoot, type));
}
// find in children
for (let c = 0; c < element.children.length; c++) {
rv.push(...findElementOfType(element.children[c], type));
}
return rv;
}
const ALPHA_THRESHOLD = 2;
function extractFrame(image, frameWidth, frameHeight, frameIndex) {
let canvas = document.createElement('canvas');
canvas.width = frameWidth;
canvas.height = frameHeight;
let context = canvas.getContext('2d');
if (!context)
return null;
let gridWidth = Math.round(image.naturalWidth / frameWidth);
let gridX = frameIndex % gridWidth;
let gridY = Math.floor(frameIndex / gridWidth);
context.drawImage(image, gridX * frameWidth, gridY * frameHeight, frameWidth, frameHeight, 0, 0, frameWidth, frameHeight);
return context.getImageData(0, 0, frameWidth, frameHeight);
}
function getHighestRGBA(datas, x, y) {
let lastData = datas[datas.length - 1];
for (let i = 0; i < datas.length; i++) {
let img = datas[i];
let xx = x + img.x;
let yy = y + img.y;
if (xx < 0 || xx >= img.imagedata.width || yy < 0 || yy >= img.imagedata.height)
continue;
let index = (yy * img.imagedata.width + xx) * 4;
let lastIndex = (y * lastData.imagedata.width + x) * 4;
if (img.imagedata.data[index + 3] > ALPHA_THRESHOLD && lastData.imagedata.data[lastIndex + 3] > ALPHA_THRESHOLD) {
return { r: img.imagedata.data[index], g: img.imagedata.data[index + 1], b: img.imagedata.data[index + 2], a: img.imagedata.data[index + 3] };
}
}
return { r: 0, g: 0, b: 0, a: 0 };
}
function ditherData(imageDatas, priorityData, randomness, percentage, x, y, frameWidth, frameHeight) {
let rv = new ImageData(frameWidth * 3, frameHeight * 3);
let m = Math.round(1 / percentage); // which nth pixel should be displayed
let r = Math.floor(randomness * m); // which nth pixel am I (everyone has different nth pixel)
for (let i = 0; i < frameWidth; i++) {
for (let j = 0; j < frameHeight; j++) {
let rgba = getHighestRGBA(imageDatas, i, j);
if (rgba.a < ALPHA_THRESHOLD)
continue;
let imageIndex = (j * frameWidth + i) * 4;
let middlePixelIndex = ((j * 3 + 1) * rv.width + i * 3 + 1) * 4;
let alpha = priorityData ? priorityData.data[imageIndex] : rgba.a;
let p = percentage > 0.99 ? 1 : Math.ceil(m / (alpha / 200));
if (negativeSafeModulo(i + x + (j + y) * 2 + r, p) !== 0) {
continue;
}
rv.data[middlePixelIndex] = rgba.r;
rv.data[middlePixelIndex + 1] = rgba.g;
rv.data[middlePixelIndex + 2] = rgba.b;
rv.data[middlePixelIndex + 3] = alpha > ALPHA_THRESHOLD ? 255 : 0;
}
}
return rv;
}
class ImageLoadHelper {
constructor(name, sources) {
this.imageLoader = new Image();
this.imageBitmap = undefined;
this.loading = false;
this.name = name;
this.sources = sources || [];
if (this.sources.length === 0)
return; // do not attach imageLoader to DOM
this.imageLoader.style.position = 'absolute';
this.imageLoader.style.top = '0';
this.imageLoader.style.left = '0';
this.imageLoader.style.width = '1px';
this.imageLoader.style.height = '1px';
this.imageLoader.style.opacity = `${Number.MIN_VALUE}`;
this.imageLoader.style.pointerEvents = 'none';
document.body.appendChild(this.imageLoader); // firefox doesn't seem to load images outside of DOM
// set image loader event listeners
this.imageLoader.addEventListener('load', () => {
if (!this.name) {
this.name = getFileStemFromUrl(this.imageLoader.src);
}
this.loading = false;
});
this.imageLoader.addEventListener('error', () => {
this.loading = false;
// assume loading from this source fails
this.sources.shift();
});
this.tryLoadSource();
}
tryLoadSource() {
if (this.loading)
return;
if (this.sources.length === 0)
return;
this.loading = true;
let candidateSource = this.sources[0];
let displayName = this.name ? this.name + ': ' : '';
console.log(`${displayName}trying to load ${candidateSource}`);
GM.xmlHttpRequest({
method: 'GET',
url: candidateSource,
responseType: 'blob',
onload: (response) => {
if (response.status === 200) {
let a = new FileReader();
a.onload = (e) => {
this.imageLoader.src = e.target.result.toString();
};
a.readAsDataURL(response.response);
}
else
this.sources.shift();
}
});
}
getImage() {
if (!this.imageLoader.complete || !this.imageLoader.src) {
this.tryLoadSource();
return;
}
return this.imageLoader;
}
destroy() {
var _a;
(_a = this.imageLoader.parentElement) === null || _a === void 0 ? void 0 : _a.removeChild(this.imageLoader);
this.imageLoader = new Image();
}
}
class Template {
constructor(params, contact, globalCanvas, priority) {
var _a, _b;
this.canvasElement = document.createElement('canvas');
this.needsCanvasInitialization = true;
// assign params
this.name = params.name;
this.sources = params.sources;
this.priorityMaskSources = params.priorityMaskSources;
this.x = params.x;
this.y = params.y;
this.frameWidth = params.frameWidth;
this.frameHeight = params.frameHeight;
this.frameCount = params.frameCount || 1;
this.frameSpeed = params.frameRate || params.frameSpeed || Infinity;
this.startTime = params.startTime || 0;
this.looping = params.looping || this.frameCount > 1;
// assign from arguments
this.globalCanvas = globalCanvas;
this.priority = priority;
//calulate from consts
let period = SECONDS_SPENT_BLINKING * 1000 / AMOUNT_OF_BLINKING;
this.blinkingPeriodMillis = Math.floor(period / UPDATE_PERIOD_MILLIS) * UPDATE_PERIOD_MILLIS;
this.animationDuration = (this.frameCount * this.frameSpeed);
//initialize image loaders
this.imageLoader = new ImageLoadHelper(this.name, this.sources);
this.priorityMaskLoader = new ImageLoadHelper(this.name, this.priorityMaskSources);
// add contact info container
this.contactX = Math.round(this.x / 5) * 5;
this.contactY = Math.round(this.y / 5) * 5;
if (contact) {
let checkingCoords = true;
while (checkingCoords) {
checkingCoords = false;
let contactInfos = this.globalCanvas.parentElement.querySelectorAll('.iHasContactInfo');
for (let i = 0; i < contactInfos.length; i++) {
let child = contactInfos[i];
let childX = parseInt((_a = child.getAttribute('contactX')) !== null && _a !== void 0 ? _a : '0');
let childY = parseInt((_b = child.getAttribute('contactY')) !== null && _b !== void 0 ? _b : '0');
let thisRight = this.contactX + 35;
let childRight = childX + 35;
let collision = this.contactX <= childRight && this.contactX >= childX || thisRight <= childRight && thisRight >= childX;
if (child
&& collision
&& Math.round(childY) === Math.round(this.contactY)) {
checkingCoords = true;
this.contactX += 5;
this.contactY += 5;
break;
}
}
}
this.contactElement = document.createElement('div');
this.contactElement.setAttribute('contactX', this.contactX.toString());
this.contactElement.setAttribute('contactY', this.contactY.toString());
this.contactElement.style.left = `${this.contactX}px`;
this.contactElement.style.top = `${this.contactY}px`;
let contactPriority = Math.round(Number.MIN_SAFE_INTEGER / 100 + priority);
this.contactElement.setAttribute('priority', contactPriority.toString());
this.contactElement.className = 'iHasContactInfo';
if (params.name) {
this.contactElement.appendChild(document.createTextNode(params.name));
this.contactElement.appendChild(document.createElement('br'));
this.contactElement.appendChild(document.createTextNode(`contact: `));
}
this.contactElement.appendChild(document.createTextNode(contact));
this.insertPriorityElement(this.contactElement);
this.initialContactCSS = getComputedStyle(this.contactElement);
}
}
updateStyle(globalRatio, left, top, translate, transform, zIndex) {
this.canvasElement.style.width = `${this.frameWidth * globalRatio}px`;
this.canvasElement.style.height = `${this.frameHeight * globalRatio}px`;
if (left !== "auto")
this.canvasElement.style.left = `calc(${this.x * globalRatio}px + ${left})`;
else
this.canvasElement.style.left = `${this.x * globalRatio}px`;
if (top !== "auto")
this.canvasElement.style.top = `calc(${this.y * globalRatio}px + ${top})`;
else
this.canvasElement.style.top = `${this.y * globalRatio}px`;
this.canvasElement.style.translate = translate;
this.canvasElement.style.transform = transform;
this.canvasElement.style.zIndex = zIndex;
if (this.contactElement) {
if (left !== "auto")
this.contactElement.style.left = `calc(${this.contactX * globalRatio}px + ${left})`;
else
this.contactElement.style.left = `${this.contactX * globalRatio}px`;
if (top !== "auto")
this.contactElement.style.top = `calc(${this.contactY * globalRatio}px + ${top})`;
else
this.contactElement.style.top = `${this.contactY * globalRatio}px`;
this.contactElement.style.maxWidth = `${30 * globalRatio}px`;
this.contactElement.style.padding = `${globalRatio}px`;
this.contactElement.style.fontSize = `${globalRatio}px`;
this.contactElement.style.translate = translate;
this.contactElement.style.transform = transform;
this.contactElement.style.zIndex = zIndex;
}
}
setContactInfoDisplay(enabled) {
if (this.contactElement) {
this.contactElement.style.opacity = enabled ? "1" : "0";
this.contactElement.style.pointerEvents = enabled ? "auto" : "none";
}
}
setPreviewMode(enabled) {
var _a;
let data = enabled ? this.fullImageData : this.ditheredData;
this.canvasElement.width = data.width;
this.canvasElement.height = data.height;
(_a = this.canvasElement.getContext('2d')) === null || _a === void 0 ? void 0 : _a.putImageData(data, 0, 0);
}
hideTemplate(enabled) {
this.canvasElement.style.opacity = enabled ? "0" : "1";
}
getCurrentFrameIndex(currentSeconds) {
if (!this.looping && this.startTime + this.frameCount * this.frameSpeed < currentSeconds)
return this.frameCount - 1;
return negativeSafeModulo(Math.floor((currentSeconds - this.startTime) / this.frameSpeed), this.frameCount);
}
insertPriorityElement(element) {
let container = this.globalCanvas.parentElement;
let priorityElements = container.children;
let priorityElementsArray = Array.from(priorityElements).filter(el => el.hasAttribute('priority'));
if (priorityElementsArray.length === 0) {
container.appendChild(element);
}
else {
priorityElementsArray.push(element);
priorityElementsArray.sort((a, b) => parseInt(b.getAttribute('priority')) - parseInt(a.getAttribute('priority')));
let index = priorityElementsArray.findIndex(el => el === element);
if (index === priorityElementsArray.length - 1) {
container.appendChild(element);
}
else {
container.insertBefore(element, priorityElementsArray[index + 1]);
}
}
}
initCanvasIfNeeded(image) {
if (this.needsCanvasInitialization) {
if (!this.frameWidth || !this.frameHeight) {
this.frameWidth = image.naturalWidth;
this.frameHeight = image.naturalHeight;
}
this.canvasElement.style.position = 'absolute';
this.canvasElement.style.top = `${this.y}px`;
this.canvasElement.style.left = `${this.x}px`;
this.canvasElement.style.width = `${this.frameWidth}px`;
this.canvasElement.style.height = `${this.frameHeight}px`;
this.canvasElement.style.pointerEvents = 'none';
this.canvasElement.style.imageRendering = 'pixelated';
this.canvasElement.setAttribute('priority', this.priority.toString());
this.insertPriorityElement(this.canvasElement);
this.needsCanvasInitialization = false;
}
}
frameStartTime(n = null) {
return (this.startTime + (n || this.currentFrame || 0) * this.frameSpeed) % this.animationDuration;
}
update(higherTemplates, percentage, randomness, currentSeconds) {
var _a;
// return if the animation is finished
if (!this.looping && currentSeconds > this.startTime + this.frameSpeed * this.frameCount) {
return;
}
let image = this.imageLoader.getImage();
let priorityMask = this.priorityMaskLoader.getImage();
// return if image isn't loaded yet
if (!image)
return;
// else initialize canvas
this.initCanvasIfNeeded(image);
// return if canvas not initialized (works because last step of canvas initialization is inserting it to DOM)
if (!this.canvasElement.isConnected) {
return;
}
// set percentage for animated
let frameIndex = this.getCurrentFrameIndex(currentSeconds);
if (this.frameCount > 1 && this.frameSpeed > 30) {
let framePast = currentSeconds % this.animationDuration - this.frameStartTime(frameIndex);
let framePercentage = framePast / this.frameSpeed;
if (framePercentage < 0.5) {
percentage *= ANIMATION_DEFAULT_PERCENTAGE;
}
}
// update canvas if necessary
if (this.currentFrame !== frameIndex || this.currentPercentage !== percentage || this.currentRandomness !== randomness) {
if (!this.frameData || this.frameCount > 1)
this.frameData = extractFrame(image, this.frameWidth, this.frameHeight, frameIndex);
if (!this.frameData)
return;
if (priorityMask) {
if (!this.priorityData || this.frameCount > 1) {
this.priorityData = extractFrame(priorityMask, this.frameWidth, this.frameHeight, frameIndex);
}
}
let frameDatas = [];
for (let i = 0; i < higherTemplates.length; i++) {
let other = higherTemplates[i];
if (this.checkCollision(other) && other.frameData)
frameDatas.push({ imagedata: other.frameData, x: this.x - other.x, y: this.y - other.y });
// the x, y over here are our coords in relation to the other template
}
frameDatas.push({ imagedata: this.frameData, x: 0, y: 0 });
this.fullImageData = frameDatas[frameDatas.length - 1].imagedata;
this.ditheredData = ditherData(frameDatas, this.priorityData, randomness, percentage, this.x, this.y, this.frameWidth, this.frameHeight);
this.canvasElement.width = this.ditheredData.width;
this.canvasElement.height = this.ditheredData.height;
(_a = this.canvasElement.getContext('2d')) === null || _a === void 0 ? void 0 : _a.putImageData(this.ditheredData, 0, 0);
}
// update done
this.currentPercentage = percentage;
this.currentFrame = frameIndex;
this.currentRandomness = randomness;
this.blinking(currentSeconds);
}
checkCollision(other) {
if (!this.frameWidth || !this.frameHeight || !other.frameWidth || !other.frameHeight)
return false;
let thisRight = this.x + this.frameWidth;
let thisBottom = this.y + this.frameHeight;
let otherRight = other.x + other.frameWidth;
let otherBottom = other.y + other.frameHeight;
if (this.x > otherRight || // this template is to the right of the other template
thisRight < other.x || // this template is to the left of the other template
this.y > otherBottom || // this template is below the other template
thisBottom < other.y // this template is above the other template
) {
return false;
}
return true;
}
blinking(currentSeconds) {
// return if no blinking needed
if (this.frameSpeed === Infinity || this.frameSpeed < 30 || this.frameCount === 1)
return;
let frameEndTime = this.frameStartTime() + this.frameSpeed;
let blinkTime = (currentSeconds % this.animationDuration) + (AMOUNT_OF_BLINKING * this.blinkingPeriodMillis / 1000);
if (blinkTime > frameEndTime) {
let blinkDiff = blinkTime - frameEndTime;
this.canvasElement.style.opacity = Math.floor(blinkDiff / (this.blinkingPeriodMillis / 1000)) % 2 === 0 ? '0' : '1';
}
else {
this.canvasElement.style.opacity = '1';
}
}
destroy() {
var _a, _b, _c;
this.imageLoader.destroy();
this.priorityMaskLoader.destroy();
(_a = this.canvasElement.parentElement) === null || _a === void 0 ? void 0 : _a.removeChild(this.canvasElement);
this.canvasElement = document.createElement('canvas');
(_c = (_b = this.contactElement) === null || _b === void 0 ? void 0 : _b.parentElement) === null || _c === void 0 ? void 0 : _c.removeChild(this.contactElement);
this.contactElement = undefined;
}
async fakeReload(time) {
this.canvasElement.style.opacity = '0';
await sleep(300 + time);
this.canvasElement.style.opacity = '1';
}
}
const context = new AudioContext();
class UserscriptAudio {
constructor(_src) {
this.ready = false;
if (_src)
this.src = _src;
}
load() {
return new Promise((resolve, reject) => {
if (!this.src)
return reject(new Error('Source is not set.'));
const error = (errText) => {
return (err) => {
console.error(`failed to load the sound from source`, this.src, ':', err);
reject(new Error(errText));
};
};
GM.xmlHttpRequest({
method: 'GET',
url: this.src,
responseType: 'arraybuffer',
onload: (response) => {
const errText = 'Failed to decode audio';
try {
context.decodeAudioData(response.response, (buffer) => {
this._buffer = buffer;
this.ready = true;
resolve();
}, error(errText));
}
catch (e) {
error(errText)(e);
}
},
onerror: error('Failed to fetch audio from URL')
});
});
}
play() {
if (!this.ready || !this._buffer) {
throw new Error('Audio not ready. Please load the audio with .load()');
}
if (this._sound) {
try {
this._sound.disconnect(context.destination);
}
catch (_a) { }
}
this._sound = context.createBufferSource();
this._sound.buffer = this._buffer;
this._sound.connect(context.destination);
this._sound.start(0);
}
}
const NOTIFICATION_SOUND_SETTINGS_KEY = 'notificationSound';
const DEFAULT_NOTIFICATION_SOUND_URL = 'https://files.catbox.moe/c9nwlu.mp3';
class NotificationManager {
constructor() {
this.container = document.createElement('div');
this.container.id = 'osuplaceNotificationContainer';
document.body.appendChild(this.container);
this.getNotificationSound()
.then((src) => {
this.initNotificationSound(src)
.catch((ex) => {
console.error('failed to init notification sound:', ex);
this.newNotification('notifications manager', 'Failed to load the notifications sound. It will not play.');
});
});
}
async getNotificationSound() {
return await GM.getValue(NOTIFICATION_SOUND_SETTINGS_KEY, DEFAULT_NOTIFICATION_SOUND_URL);
}
async setNotificationSound(sound) {
await this.initNotificationSound(sound);
await GM.setValue(NOTIFICATION_SOUND_SETTINGS_KEY, sound);
}
async initNotificationSound(src) {
const newAudio = new UserscriptAudio(src);
await newAudio.load();
this.notificationSound = newAudio;
}
newNotification(url, message) {
let div = document.createElement('div');
div.appendChild(wrapInHtml('i', `${url} says:`));
div.append(document.createElement('br'));
div.append(wrapInHtml('b', message));
div.className = 'osuplaceNotification';
div.onclick = () => {
div.classList.remove('visible');
setTimeout(() => div.remove(), 500);
};
this.container.appendChild(div);
setTimeout(() => {
div.classList.add('visible');
}, 100);
if (this.notificationSound) {
try {
this.notificationSound.play();
}
catch (err) {
console.error('failed to play notification audio', err);
}
}
}
}
const WS_FORCE_CLOSE_CODE = 3006;
class TemplateManager {
constructor(canvasElements, startingUrl) {
this.templatesToLoad = MAX_TEMPLATES;
this.alreadyLoaded = new Array();
this.websockets = new Map();
this.intervals = new Map();
this.seenNotifications = new Array();
this.notificationTypes = new Map();
this.enabledNotifications = new Array();
this.whitelist = new Array();
this.blacklist = new Array();
this.templateConstructors = new Array();
this.templates = new Array();
this.responseDiffs = new Array();
this.canvasElements = [];
this.randomness = Math.random();
this.percentage = 1;
this.lastCacheBust = this.getCacheBustString();
this.notificationManager = new NotificationManager();
this.notificationSent = false;
this.contactInfoEnabled = false;
this.showTopLevelNotification = true;
console.log('TemplateManager constructor ', canvasElements, window.location);
this.canvasElements = canvasElements;
this.selectedCanvas = canvasElements[0];
this.selectBestCanvas();
this.startingUrl = startingUrl;
this.initOrReloadTemplates(true);
GM.getValue(`${window.location.host}_notificationsEnabled`, "[]").then((value) => {
this.enabledNotifications = JSON.parse(value);
});
let style = document.createElement('style');
style.id = 'osuplace-contactinfo-style';
style.innerHTML = CONTACT_INFO_CSS;
this.selectedCanvas.parentElement.appendChild(style);
let globalStyle = document.createElement("style");
globalStyle.innerHTML = GLOBAL_CSS;
document.body.appendChild(globalStyle);
this.canvasObserver = new MutationObserver(() => {
let css = getComputedStyle(this.selectedCanvas);
let left = css.left;
let top = css.top;
let translate = css.translate;
let transform = css.transform;
let zIndex = css.zIndex;
let globalRatio = parseFloat(this.selectedCanvas.style.width) / this.selectedCanvas.width;
for (let i = 0; i < this.templates.length; i++) {
this.templates[i].updateStyle(globalRatio, left, top, translate, transform, zIndex);
}
});
this.canvasObserver.observe(this.selectedCanvas, { attributes: true });
setInterval(() => {
const now = Math.floor(+new Date() / 1000);
this.seenNotifications = this.seenNotifications.filter((d) => d && ((d.seenAt - now) < 10));
}, 60 * 1000);
}
selectBestCanvas() {
var _a, _b, _c;
let selectionChanged = false;
let selectedBounds = this.selectedCanvas.getBoundingClientRect();
for (let i = 0; i < this.canvasElements.length; i++) {
let canvas = this.canvasElements[i];
let canvasBounds = canvas.getBoundingClientRect();
let selectedArea = selectedBounds.width * selectedBounds.height;
let canvasArea = canvasBounds.width * canvasBounds.height;
if (canvasArea > selectedArea) {
this.selectedCanvas = canvas;
selectedBounds = canvasBounds;
selectionChanged = true;
}
}
if (selectionChanged) {
while (this.templates.length) {
(_a = this.templates.shift()) === null || _a === void 0 ? void 0 : _a.destroy();
}
for (let i = 0; i < this.templateConstructors.length; i++) {
this.templates.push(this.templateConstructors[i](this.selectedCanvas));
this.sortTemplates();
}
(_b = this.canvasObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
(_c = this.canvasObserver) === null || _c === void 0 ? void 0 : _c.observe(this.selectedCanvas, { attributes: true });
}
}
getCacheBustString() {
return Math.floor(Date.now() / CACHE_BUST_PERIOD).toString(36);
}
loadTemplatesFromJsonURL(url, minPriority = 0, lastContact = '') {
let _url = new URL(url);
let uniqueString = `${_url.origin}${_url.pathname}`;
// exit if already loaded
// exit if blacklisted
if (this.alreadyLoaded.includes(uniqueString) || this.blacklist.includes(uniqueString))
return;
this.alreadyLoaded.push(uniqueString);
console.log(`loading template from ${_url}`);
// do some cache busting
this.lastCacheBust = this.getCacheBustString();
_url.searchParams.append("date", this.lastCacheBust);
GM.xmlHttpRequest({
method: 'GET',
url: _url.href,
onload: (response) => {
// use this request to callibrate the latency to general internet requests
let responseMatch = response.responseHeaders.match(/date:(.*)\r/i);
if (responseMatch) {
let responseTime = Date.parse(responseMatch[1]);
this.responseDiffs.push(responseTime - Date.now());
}
// parse the response
let json = JSON.parse(response.responseText);
// read blacklist. These will never be loaded
if (json.blacklist) {
for (let i = 0; i < json.blacklist.length; i++) {
this.blacklist.push(json.blacklist[i].url);
}
}
// read whitelist. These will be loaded later
if (json.whitelist) {
for (let i = 0; i < json.whitelist.length; i++) {
let entry = json.whitelist[i];
let contactInfo = json.contact || json.contactInfo || lastContact;
entry.name = entry.name ? `${entry.name}, from: ${contactInfo}` : contactInfo;
this.whitelist.push(json.whitelist[i]);
}
}
// read templates
if (json.templates) {
for (let i = 0; i < json.templates.length; i++) {
if (this.templates.length < this.templatesToLoad) {
let constructor = (a) => new Template(json.templates[i], json.contact || json.contactInfo || lastContact, a, minPriority + this.templates.length);
this.templateConstructors.push(constructor);
let newTemplate = constructor(this.selectedCanvas);
this.templates.push(newTemplate);
newTemplate.setContactInfoDisplay(this.contactInfoEnabled);
this.sortTemplates();
}
}
}
// connect to websocket
if (json.notifications) {
this.setupNotifications(json.notifications, url == this.startingUrl);
}
},
onerror: console.error
});
}
sortTemplates() {
this.templates.sort((a, b) => a.priority - b.priority);
}
setupNotifications(serverUrl, isTopLevelTemplate, doPoll = false) {
console.log('attempting to set up notification server ' + serverUrl, doPoll ? "polling" : "websocket");
// check if we're not already connected
let wsUrl = new URL('/listen', serverUrl);
wsUrl.protocol = wsUrl.protocol == 'https:' ? 'wss:' : 'ws:';
for (const socket of this.websockets.values()) {
if (socket.url == wsUrl.toString()) {
if (socket.readyState != socket.CLOSING && socket.readyState != socket.CLOSED) {
console.log(`we are already connected to ${wsUrl}, skipping!`);
return;
}
}
}
// get topics
let domain = new URL(serverUrl).hostname.replace(/[\.\-_]?broadcaster/, '');
if (domain[0] === '.')
domain = domain.substring(1);
// do some cache busting
let _url = new URL(serverUrl + "/topics");
this.lastCacheBust = this.getCacheBustString();
_url.searchParams.append("date", this.lastCacheBust);
GM.xmlHttpRequest({
method: 'GET',
url: _url.href,
responseType: 'text',
onload: async (response) => {
if (response.status !== 200) {
console.error(`error getting ${serverUrl}/topics, trying again in 10s...`);
setTimeout(() => { this.setupNotifications(serverUrl, isTopLevelTemplate); }, 10000);
return false;
}
let data = response.response;
try {
data = JSON.parse(data);
}
catch (ex) {
console.error(`error parsing ${serverUrl} topics: ${ex}, trying again in 10s...`);
setTimeout(() => { this.setupNotifications(serverUrl, isTopLevelTemplate); }, 10000);
return false;
}
if (data == false)
return;
let topics = [];
data.forEach((topicFromApi) => {
if (!topicFromApi.id || !topicFromApi.description) {
console.error('Invalid topic: ' + topicFromApi);
return;
}
let topic = topicFromApi;
if (isTopLevelTemplate) {
topic.forced = true;
removeItem(this.enabledNotifications, `${domain}??${topic.id}`);
this.enabledNotifications.push(`${domain}??${topic.id}`);
}
topics.push(topic);
});
this.notificationTypes.set(domain, topics);
if (isTopLevelTemplate) {
let enabledKey = `${window.location.host}_notificationsEnabled`;
await GM.setValue(enabledKey, JSON.stringify(this.enabledNotifications));
if (this.showTopLevelNotification) {
this.notificationManager.newNotification("template manager", `You were automatically set to recieve notifications from ${domain} as it's from your address-bar template`);
this.showTopLevelNotification = false;
}
}
const handleNotificationEvent = (data) => {
// https://github.com/osuplace/broadcaster/blob/main/API.md
if (data.e == 1) {
if (!data.t || !data.c) {
console.error(`Malformed event from ${serverUrl}: ${data}`);
}
let topic = topics.find(t => t.id == data.t); // FIXME: if we add dynamically updating topics, this will use the old topic list instead of the up to date one
if (!topic)
return;
if (data.i) {
const id = `${domain}:${data.i}`;
if (this.seenNotifications.some((v) => v.id == id))
return;
this.seenNotifications.push({
id,
seenAt: Math.floor(+new Date() / 1000)
});
}
if (this.enabledNotifications.includes(`${domain}??${data.t}`) || topic.forced) {
this.notificationManager.newNotification(domain, data.c);
}
}
else {
console.log(`Received unknown event from ${serverUrl}: ${data}`);
}
};
if (doPoll) {
let timer = setInterval(() => {
if (!this.enabledNotifications.some((en) => en.startsWith(domain)))
return;
let pollUrl = new URL(serverUrl + "/listen-poll");
pollUrl.searchParams.append("date", (+new Date()).toString());
GM.xmlHttpRequest({
method: 'GET',
url: pollUrl.href,
responseType: 'text',
onload: async (response) => {
if (response.status === 404) {
console.error(`${serverUrl} does not have polling support, trying again with websocket in 30s...`);
setTimeout(() => { this.setupNotifications(serverUrl, isTopLevelTemplate); }, 30000);
clearInterval(timer);
return false;
}
if (response.status !== 200) {
console.error(`error getting ${serverUrl}/listen-poll, trying again in 10s...`);
setTimeout(() => { this.setupNotifications(serverUrl, isTopLevelTemplate); }, 10000);
clearInterval(timer);
return false;
}
let data = response.response;
try {
data = JSON.parse(data);
if (!Array.isArray(data))
throw new Error();
}
catch (ex) {
console.error(`error parsing ${serverUrl} listen (poll): ${ex}, trying again in 10s...`);
setTimeout(() => { this.setupNotifications(serverUrl, isTopLevelTemplate); }, 10000);
clearInterval(timer);
return false;
}
for (const event of data)
handleNotificationEvent(event);
},
onerror: (err) => {
console.error(`error getting ${serverUrl}/listen-poll, trying again in 10s...`, err);
setTimeout(() => { this.setupNotifications(serverUrl, isTopLevelTemplate); }, 10000);
clearInterval(timer);
}
});
}, 10 * 1000);
if (this.intervals.has(domain))
clearInterval(this.intervals.get(domain));
this.intervals.set(domain, timer);
}
else {
// actually connecting to the websocket now
let ws = new WebSocket(wsUrl);
ws.addEventListener('open', (_) => {
var _a;
console.log(`successfully connected to websocket for ${serverUrl}`);
if (this.websockets.has(domain))
(_a = this.websockets.get(domain)) === null || _a === void 0 ? void 0 : _a.close(WS_FORCE_CLOSE_CODE);
this.websockets.set(domain, ws);
});
ws.addEventListener('message', async (event) => {
let data = JSON.parse(await event.data);
handleNotificationEvent(data);
});
ws.addEventListener('close', (event) => {
if (event.code === WS_FORCE_CLOSE_CODE)
return;
console.log(`websocket on ${ws.url} closing!`);
this.websockets.delete(domain);
setTimeout(() => {
this.setupNotifications(serverUrl, isTopLevelTemplate);
}, 1000 * 30);
});
ws.addEventListener('error', (_) => {
console.log(`websocket error on ${ws.url}, closing!`);
ws.close();
console.error(`failed to create a websocket connection to ${serverUrl}, trying polling...`);
setTimeout(() => {
this.setupNotifications(serverUrl, isTopLevelTemplate, true);
}, 1000 * 1);
});
}
},
onerror: (error) => {
console.error(`Couldn\'t get topics from ${serverUrl}: ${error}`);
}
});
}
canReload() {
return this.lastCacheBust !== this.getCacheBustString();
}
initOrReloadTemplates(forced = false, contactInfo = null) {
var _a;
if (contactInfo !== null)
this.contactInfoEnabled = contactInfo;
this.setContactInfoDisplay(this.contactInfoEnabled);
if (!this.canReload() && !forced) {
// fake a reload
for (let i = 0; i < this.templates.length; i++) {
this.templates[i].fakeReload(i * 50);
}
return;
}
// reload the templates
// reloading only the json is not possible because it's user input and not uniquely identifiable
// so everything is reloaded as if the template manager was just initialized
while (this.templates.length) {
(_a = this.templates.shift()) === null || _a === void 0 ? void 0 : _a.destroy();
}
for (const ws of this.websockets.values()) {
console.log('initOrReloadTemplates is closing connection ' + ws.url);
ws === null || ws === void 0 ? void 0 : ws.close(WS_FORCE_CLOSE_CODE);
}
for (const interval of this.intervals.values()) {
clearInterval(interval);
}
this.templates = [];
this.websockets.clear();
this.intervals.clear();
this.alreadyLoaded = [];
this.whitelist = [];
this.blacklist = [];
if (this.startingUrl !== NO_JSON_TEMPLATE_IN_PARAMS)
this.loadTemplatesFromJsonURL(this.startingUrl);
GM.getValue(`${window.location.host}_alwaysLoad`).then(value => {
if (value && value !== "[]") {
let templates = JSON.parse(value);
for (let i = 0; i < templates.length; i++) {
this.loadTemplatesFromJsonURL(templates[i]);
}
}
else if (!this.notificationSent) {
this.notificationManager.newNotification("template manager", "No default template set. Consider adding one via settings.");
this.notificationSent = true;
}
});
}
currentSeconds() {
let averageDiff = this.responseDiffs.reduce((a, b) => a + b, 0) / (this.responseDiffs.length);
return (Date.now() + averageDiff) / 1000;
}
update() {
this.selectBestCanvas();
let cs = this.currentSeconds();
for (let i = 0; i < this.templates.length; i++) {
try {
this.templates[i].update(this.templates.slice(0, i), this.percentage, this.randomness, cs);
}
catch (e) {
console.log(`failed to update template ${this.templates[i].name}`);
}
}
if (this.templates.length < this.templatesToLoad) {
for (let i = 0; i < this.whitelist.length; i++) {
// yes this calls all whitelist all the time but the load will cancel if already loaded
let entry = this.whitelist[i];
this.loadTemplatesFromJsonURL(entry.url, i * this.templatesToLoad, entry.name);
}
}
}
setContactInfoDisplay(enabled) {
for (let i = 0; i < this.templates.length; i++) {
this.templates[i].setContactInfoDisplay(enabled);
}
}
setPreviewMode(enabled) {
for (let i = 0; i < this.templates.length; i++) {
this.templates[i].setPreviewMode(enabled);
}
}
hideTemplate(enabled) {
for (let i = 0; i < this.templates.length; i++) {
this.templates[i].hideTemplate(enabled);
}
}
}
function createLabel(text) {
let label = document.createElement("label");
label.innerText = text;
return label;
}
function createButton(text, callback) {
let button = document.createElement("button");
button.innerText = text;
button.onclick = () => callback();
button.className = "settingsButton";
return button;
}
function createTextInput(buttonText, placeholder, callback) {
let div = document.createElement("div");
let textInput = document.createElement("input");
textInput.type = "text";
textInput.placeholder = placeholder;
textInput.className = "settingsTextInput";
let button = createButton(buttonText, () => {
callback(textInput.value, textInput);
});
div.appendChild(textInput);
div.appendChild(button);
return div;
}
function createSlider(Text, value, callback) {
let div = document.createElement("div");
div.className = "settingsSliderBox";
let slider = document.createElement("input");
slider.type = "range";
slider.min = '0';
slider.max = '100';
slider.step = '1';
slider.value = value;
slider.oninput = (ev) => {
ev.preventDefault();
callback(parseInt(slider.value));
};
slider.style.width = "100%";
let label = document.createElement("label");
label.innerText = Text;
label.style.color = "#eee";
div.append(label);
div.appendChild(document.createElement("br"));
div.append(slider);
return div;
}
function createBoldCheckbox(boldText, regularText, checked, callback, disabled = false) {
let div = document.createElement("div");
div.className = "settingsCheckbox";
let checkbox = document.createElement('input');
checkbox.type = "checkbox";
checkbox.checked = checked;
checkbox.disabled = disabled;
checkbox.oninput = (ev) => {
ev.preventDefault();
callback(checkbox.checked);
};
let label = document.createElement("label");
let b = document.createElement("b");
b.innerText = boldText;
label.append(b);
label.append(document.createTextNode(regularText));
label.style.color = "#eee";
div.append(checkbox);
div.append(label);
return div;
}
class Settings {
constructor(manager) {
this.overlay = document.createElement("div");
this.templateLinksWrapper = document.createElement("div");
this.notificationsWrapper = document.createElement("div");
this.toolsWrapper = document.createElement("div");
this.reloadTemplatesWhenClosed = false;
this.contactInfoEnabled = false;
this.previewModeEnabled = false;
this.hideTemplate = false;
this.templateLinksWrapper.className = "settingsWrapper";
this.templateLinksWrapper.id = "templateLinksWrapper";
this.notificationsWrapper.className = "settingsWrapper";
this.manager = manager;
document.body.appendChild(this.overlay);
this.overlay.id = "settingsOverlay";
this.overlay.style.opacity = "0";
let div = document.createElement('div');
div.className = "settingsWrapper";
div.appendChild(createLabel(".json Template settings - v" + GM.info.script.version));
div.appendChild(document.createElement('br'));
div.appendChild(createButton("Reload the template", () => manager.initOrReloadTemplates(false, this.contactInfoEnabled)));
div.appendChild(document.createElement('br'));
div.appendChild(createSlider("Templates to load", "4", (n) => {
manager.templatesToLoad = (n + 1) * MAX_TEMPLATES / 5;
}));
div.appendChild(document.createElement('br'));
div.appendChild(createButton("Generate new randomness", () => {
let currentRandomness = manager.randomness;
while (true) {
manager.randomness = Math.random();
if (Math.abs(currentRandomness - manager.randomness) > 1 / 3)
break;
}
}));
div.appendChild(document.createElement('br'));
div.appendChild(createSlider("Dither amount", "1", (n) => {
var _a;
manager.percentage = 1 / (n / 10 + 1);
if (this.previewModeEnabled) {
// disable 'preview template in full', because changing percentage
// overrides the template rendering anyway
this.previewModeEnabled = false;
const previewModeInput = (_a = this.previewModeCheckbox) === null || _a === void 0 ? void 0 : _a.children[0];
if (previewModeInput)
previewModeInput.checked = false;
}
}));
div.appendChild(document.createElement('br'));
div.appendChild(createBoldCheckbox('', "Show contact info besides templates", this.contactInfoEnabled, (a) => {
manager.setContactInfoDisplay(a);
this.contactInfoEnabled = a;
}));
this.previewModeCheckbox = div.appendChild(createBoldCheckbox('', "Preview template in full", this.previewModeEnabled, (a) => {
manager.setPreviewMode(a);
this.previewModeEnabled = a;
}));
div.appendChild(createBoldCheckbox('', "Hide template", this.hideTemplate, (a) => {
manager.hideTemplate(a);
this.hideTemplate = a;
}));
this.populateSoundOptions(div);
div.appendChild(document.createElement('br'));
let clickHandler = document.createElement('div');
clickHandler.style.width = '100vw';
clickHandler.style.height = '100vh';
clickHandler.style.position = 'absolute';
clickHandler.style.left = '-0.1px';
clickHandler.style.right = '-0.1px';
clickHandler.style.overflowY = 'auto';
clickHandler.addEventListener("wheel", (ev) => {
ev.preventDefault();
var direction = (ev.deltaY > 0) ? 1 : -1;
clickHandler.scrollTop += direction * 100;
});
clickHandler.onclick = (ev) => {
if (ev.target === ev.currentTarget)
this.close();
};
window.addEventListener("keydown", (ev) => {
if (ev.key === "Escape") {
this.close();
}
});
this.overlay.attachShadow({ mode: 'open' });
let globalStyle = document.createElement("style");
globalStyle.innerHTML = SETTINGS_CSS;
this.overlay.shadowRoot.appendChild(globalStyle);
this.overlay.shadowRoot.appendChild(clickHandler);
clickHandler.appendChild(div);
clickHandler.appendChild(this.templateLinksWrapper);
clickHandler.appendChild(this.notificationsWrapper);
this.populateToolsWrapper()
clickHandler.appendChild(this.toolsWrapper);
}
open() {
this.overlay.style.opacity = "1";
this.overlay.style.pointerEvents = "auto";
this.populateAll();
}
close() {
this.overlay.style.opacity = "0";
this.overlay.style.pointerEvents = "none";
if (this.reloadTemplatesWhenClosed) {
this.manager.initOrReloadTemplates(true, this.contactInfoEnabled);
this.reloadTemplatesWhenClosed = false;
}
}
toggle() {
if (this.overlay.style.opacity === "0") {
this.open();
}
else {
this.close();
}
}
changeMouseEvents(enabled) {
if (this.overlay.style.opacity === "0")
this.overlay.style.pointerEvents = enabled ? "auto" : "none";
}
populateAll() {
this.populateTemplateLinks();
this.populateNotifications();
}
populateSoundOptions(div) {
const audioDiv = document.createElement('div');
div.appendChild(document.createElement('br'));
div.appendChild(audioDiv);
this.manager.notificationManager.getNotificationSound()
.then((value) => {
let linkLabel = document.createElement('label');
let updateLinkLabel = (url) => {
linkLabel.innerHTML = `Current sound: <a target="_blank" rel="noopener noreferrer" href="${url}">${url}</a>`;
};
updateLinkLabel(value);
audioDiv.appendChild(createLabel('Set new notification sound:'));
audioDiv.appendChild(document.createElement('br'));
audioDiv.appendChild(linkLabel);
audioDiv.appendChild(createTextInput('Apply', 'Sound URL', (newSound, input) => {
if (!newSound.trim().length) {
return;
}
this.manager.notificationManager.setNotificationSound(newSound)
.then(() => {
this.manager.notificationManager.newNotification('settings', 'Applied new sound!');
input.value = '';
updateLinkLabel(newSound);
})
.catch((err) => {
this.manager.notificationManager.newNotification('settings', 'Failed to apply new sound:\n' + err);
});
}));
});
}
populateTemplateLinks() {
while (this.templateLinksWrapper.children.length) {
this.templateLinksWrapper.children[0].remove();
}
GM.getValue(`${window.location.host}_alwaysLoad`).then(value => {
let templates = value ? JSON.parse(value) : [];
let templateAdder = createTextInput("Always load", "Template URL", async (tx) => {
let url = new URL(tx);
let template = findJSONTemplateInURL(url) || url.toString();
if (templates.includes(template))
return;
templates.push(template);
await GM.setValue(`${window.location.host}_alwaysLoad`, JSON.stringify(templates));
this.populateTemplateLinks();
this.manager.loadTemplatesFromJsonURL(template);
});
this.templateLinksWrapper.appendChild(templateAdder);
if (templates.length > 0) {
this.templateLinksWrapper.appendChild(createLabel("Click to remove template from always loading"));
this.templateLinksWrapper.appendChild(document.createElement('br'));
}
for (let i = 0; i < templates.length; i++) {
let button = createButton(templates[i], async () => {
button.remove();
templates.splice(i, 1);
await GM.setValue(`${window.location.host}_alwaysLoad`, JSON.stringify(templates));
this.populateTemplateLinks();
this.reloadTemplatesWhenClosed = true;
});
button.className = `${button.className} templateLink`;
this.templateLinksWrapper.appendChild(button);
}
});
}
populateNotifications() {
while (this.notificationsWrapper.children.length) {
this.notificationsWrapper.children[0].remove();
}
let keys = this.manager.notificationTypes.keys();
let key;
while (!(key = keys.next()).done) {
let value = key.value;
this.notificationsWrapper.appendChild(createLabel(value));
let notifications = this.manager.notificationTypes.get(value);
if (notifications === null || notifications === void 0 ? void 0 : notifications.length) {
for (let i = 0; i < notifications.length; i++) {
let notification = notifications[i];
let enabled = this.manager.enabledNotifications.includes(`${value}??${notification.id}`);
if (notification.forced)
enabled = true;
let checkbox = createBoldCheckbox(notification.id + " - ", notification.description, enabled, async (b) => {
removeItem(this.manager.enabledNotifications, `${value}??${notification.id}`);
if (b) {
this.manager.enabledNotifications.push(`${value}??${notification.id}`);
}
let enabledKey = `${window.location.host}_notificationsEnabled`;
await GM.setValue(enabledKey, JSON.stringify(this.manager.enabledNotifications));
}, notification.forced);
this.notificationsWrapper.append(document.createElement('br'));
this.notificationsWrapper.append(checkbox);
}
}
this.notificationsWrapper.append(document.createElement('br'));
}
}
populateToolsWrapper() {
this.toolsWrapper.className = 'settingsWrapper';
this.toolsWrapper.appendChild(createButton("Export Image", () => {
const image = this.manager.selectedCanvas.toDataURL("image/png");
const createEl = document.createElement('a');
createEl.href = image;
// This is the name of our downloaded file
createEl.download = `rplace-${Date.now()}.png`;
// Click the download button, causing a download, and then remove it
createEl.click();
createEl.remove();
}));
this.toolsWrapper.appendChild(document.createElement('br'));
const foundLabel = createLabel(`No amongus found. Click get Amongus to update`);
this.toolsWrapper.appendChild(foundLabel)
this.toolsWrapper.appendChild(document.createElement('br'));
this.toolsWrapper.appendChild(createButton("Get Amongus", () => {
const { foundPatterns, took, overlay } = genAmongus(this.manager.selectedCanvas);
const createEl = document.createElement('a');
var image = new Image();
image.src = overlay.toDataURL("image/png");
var w = window.open("");
w.document.write(image.outerHTML);
foundLabel.innerText = `Found ${foundPatterns.length} amongus in (${took}ms)`
}));
this.toolsWrapper.appendChild(document.createElement('br'));
this.toolsWrapper.appendChild(createLabel(`Amongus Detector By loucass003`))
}
}
let SLIDERS_SVG = '<button><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M0 416c0-17.7 14.3-32 32-32l54.7 0c12.3-28.3 40.5-48 73.3-48s61 19.7 73.3 48L480 384c17.7 0 32 14.3 32 32s-14.3 32-32 32l-246.7 0c-12.3 28.3-40.5 48-73.3 48s-61-19.7-73.3-48L32 448c-17.7 0-32-14.3-32-32zm192 0a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zM384 256a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zm-32-80c32.8 0 61 19.7 73.3 48l54.7 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-54.7 0c-12.3 28.3-40.5 48-73.3 48s-61-19.7-73.3-48L32 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l246.7 0c12.3-28.3 40.5-48 73.3-48zM192 64a32 32 0 1 0 0 64 32 32 0 1 0 0-64zm73.3 0L480 64c17.7 0 32 14.3 32 32s-14.3 32-32 32l-214.7 0c-12.3 28.3-40.5 48-73.3 48s-61-19.7-73.3-48L32 128C14.3 128 0 113.7 0 96S14.3 64 32 64l86.7 0C131 35.7 159.2 16 192 16s61 19.7 73.3 48z"/></svg></button>';
async function init(manager) {
let settings = new Settings(manager);
while (window.innerWidth === 0 || window.innerHeight === 0) {
await sleep(1000);
}
let xKey = `${window.location.host}_settingsX`;
let yKey = `${window.location.host}_settingsY`;
let GMx = await GM.getValue(xKey, null) || 10;
let GMy = await GM.getValue(yKey, null) || 10;
let iconElement = stringToHtml(SLIDERS_SVG);
document.body.append(iconElement);
let setPosition = async (mouseX, mouseY) => {
let xMin = 16 / window.innerWidth * 100;
let yMin = 16 / window.innerHeight * 100;
let x = (mouseX) / window.innerWidth * 100;
let y = (mouseY) / window.innerHeight * 100;
await GM.setValue(xKey, x);
await GM.setValue(yKey, y);
if (x < 50) {
x = Math.max(xMin, x - xMin);
iconElement.style.left = `${x}vw`;
iconElement.style.right = 'unset';
}
else {
x = Math.max(xMin, 100 - x - xMin);
iconElement.style.right = `${x}vw`;
iconElement.style.left = 'unset';
}
if (y < 50) {
y = Math.max(yMin, y - yMin);
iconElement.style.top = `${y}vh`;
iconElement.style.bottom = 'unset';
}
else {
y = Math.max(yMin, 100 - y - yMin);
iconElement.style.bottom = `${y}vh`;
iconElement.style.top = 'unset';
}
};
await setPosition(GMx / 100 * window.innerWidth, GMy / 100 * window.innerHeight);
iconElement.style.position = 'absolute';
iconElement.style.width = "32px";
iconElement.style.height = "32px";
iconElement.style.backgroundColor = '#fff';
iconElement.style.padding = "5px";
iconElement.style.borderRadius = "5px";
iconElement.style.zIndex = `${Number.MAX_SAFE_INTEGER - 1}`;
iconElement.style.cursor = "pointer";
let clicked = false;
let dragged = false;
iconElement.addEventListener('mousedown', (ev) => {
if (ev.button === 0) {
clicked = true;
settings.changeMouseEvents(true);
ev.preventDefault(); // prevent text from getting selected
}
});
iconElement.addEventListener('mouseleave', (ev) => {
if (clicked) {
dragged = true;
}
});
window.addEventListener('mouseup', (ev) => {
if (ev.button === 0) {
if (clicked && !dragged) {
settings.toggle();
}
clicked = false;
dragged = false;
settings.changeMouseEvents(false);
}
});
window.addEventListener('mousemove', (ev) => {
if (dragged) {
setPosition(ev.clientX, ev.clientY);
}
});
iconElement.addEventListener('touchstart', (ev) => {
clicked = true;
});
window.addEventListener('touchend', (ev) => {
clicked = false;
dragged = false;
});
window.addEventListener('touchmove', (ev) => {
if (ev.touches.length === 1) {
if (iconElement !== document.elementFromPoint(ev.touches[0].pageX, ev.touches[0].pageY) && clicked)
dragged = true;
if (dragged)
setPosition(ev.touches[0].clientX, ev.touches[0].clientY);
}
});
}
let jsontemplate;
let canvasElements; // FIXME: This should probably be a list and the user can just select the correct one manually
function topWindow() {
console.log("top window code for", window.location.href);
GM.setValue('canvasFound', false);
let params = findJSONTemplateInURL(window.location) || NO_JSON_TEMPLATE_IN_PARAMS;
jsontemplate = params;
GM.setValue('jsontemplate', jsontemplate);
}
async function canvasWindow() {
while (document.readyState !== 'complete') {
console.log("Template manager sleeping for 1 second because document isn't ready yet.");
await sleep(1000);
}
console.log("canvas code for", window.location.href);
let sleep$1 = 0;
while (canvasElements === undefined || canvasElements.length === 0) {
if (await GM.getValue('canvasFound', false) && !windowIsEmbedded()) {
console.log('canvas found by iframe');
return;
}
await sleep(1000 * sleep$1);
sleep$1++;
console.log("trying to find canvas");
canvasElements = findElementOfType(document.documentElement, HTMLCanvasElement);
}
GM.setValue('canvasFound', true);
sleep$1 = 0;
while (true) {
if (jsontemplate) {
runCanvas(jsontemplate, canvasElements);
break;
}
else if (windowIsEmbedded()) {
jsontemplate = (await GM.getValue('jsontemplate', ''));
}
await sleep(1000 * sleep$1);
sleep$1++;
}
}
async function runCanvas(jsontemplate, canvasElements) {
let manager = new TemplateManager(canvasElements, jsontemplate);
init(manager);
window.setInterval(() => {
manager.update();
}, UPDATE_PERIOD_MILLIS);
window.setInterval(() => {
console.log("Reloading template...");
manager.initOrReloadTemplates(false, null);
}, TEMPLATE_RELOAD_INTERVAL);
GM.setValue('jsontemplate', '');
}
console.log(`running templating script in ${window.location.href}`);
if (!windowIsEmbedded()) {
// we are the top window
topWindow();
}
canvasWindow();
let __url = new URL(window.location.href);
if (__url.origin.endsWith('reddit.com')) {
run();
}
})();