Greasy Fork

Greasy Fork is available in English.

rplace amongus detector

Manages your templates on various canvas games

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
    }

})();