Infinite Worlds tweaks

Keeps the story illustrations onscreen, story keyboard navigation (1-5 for prompts, "a" and "s" to swap images, "e" to edit images), "ESC" can be used to close singe-button dialogs, adds arrow key navigation to community worlds, widens "Illustration details" window, and closes Discord popup. Keyboard navigation only works when the focus is not on an "input" element (use "ESC" to leave input focus).

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Infinite Worlds tweaks
// @namespace    http://tampermonkey.net/
// @version      1.8
// @description  Keeps the story illustrations onscreen, story keyboard navigation (1-5 for prompts, "a" and "s" to swap images, "e" to edit images), "ESC" can be used to close singe-button dialogs, adds arrow key navigation to community worlds, widens "Illustration details" window, and closes Discord popup.  Keyboard navigation only works when the focus is not on an "input" element (use "ESC" to leave input focus).
// @author       HiEv
// @license      MIT
// @match        https://infiniteworlds.app/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=infiniteworlds.app
// @grant        none
// @require      http://code.jquery.com/jquery-3.7.1.min.js
// ==/UserScript==

/*
	global $
*/

(function() {
	'use strict';

    // Styling tweaks
    $('html > head').append($(
        '<style>\n' + // Illustration image tweaks.
        '.anvil-panel-section .has-components .anvil-spacing-above-small[style="min-height: 20px; text-align: center; overflow: clip;"] > img {\n' +
        '    transition: margin-top 0.5s ease-out;\n' +
        '    width: auto !important;\n' +
        '    max-height: calc(100vh - 47px) !important;\n' +
        '}\n' +
        '@media (min-width: 768px) {\n' + // Widen the "Illustration details" window.
        '    .modal-sm {\n' +
        '        width: 800px;\n' +
        '        max-width: 90vw;\n' +
        '    }\n' +
        '}</style>'));

    // Keyboard navigation tweaks
    setTimeout(() => {
        $(document).on("keyup", function (event) { // Keyboard handler
            if (($("input:focus").length === 0) && ($("textarea:focus").length === 0) && ($("div[contenteditable='true']:focus").length == 0)) {
                if (event.key === "ArrowLeft") { // Community Worlds navigate previous
                    $('[data-page="prev"]').last().trigger("click");
                }
                if (event.key === "ArrowRight") { // Community Worlds navigate next
                    $('[data-page="next"]').last().trigger("click");
                }
                if (event.key === "e") { // Open the edit dialog
                    if ($("div.modal.alert-modal").length == 0) {
                        let links = $('[title="View/modify image instructions"] button');
                        if (links.length >= 1) {
                            links[0].click();
                        }
                    } else { // Close the edit dialog
                        $("button.close").click();
                    }
                }
                if (event.key === "a") { // Swap to previous image
                    let links = $('div[title="Previous image"] button');
                    if (links.length >= 1) {
                        links[0].click();
                    }
                }
                if (event.key === "s") { // Swap to next image
                    // let links = $('button').children("i.fa-rotate");  // old version
                    let links = $('div[title="Next image"] button');
                    if (links.length >= 1) {
                        // $(links[0]).parent().click();  // old version
                        links[0].click();
                    }
                }
                if (event.key === "1") { // Option #1
                    let links = $('[anvil-role="option-button"] button');
                    if (links.length >= 1) {
                        links[0].click();
                    }
                }
                if (event.key === "2") { // Option #2
                    let links = $('[anvil-role="option-button"] button');
                    if (links.length >= 2) {
                        links[1].click();
                    }
                }
                if (event.key === "3") { // Option #3
                    let links = $('[anvil-role="option-button"] button');
                    if (links.length >= 3) {
                        links[2].click();
                    }
                }
                if (event.key === "4") { // Action input
                    let links = $('textarea');
                    if (links.length >= 1) {
                        links[0].focus();
                    }
                }
                if (event.key === "5") { // AI input
                    let links = $('textarea');
                    if (links.length >= 2) {
                        links[1].focus();
                    }
                }
                if (event.key === "Escape") { // Close dialog with one option
                    if ($('[class="modal-content"] button:visible').length === 1) {
                        $('[class="modal-content"] button:visible').click();
                    }
                }
            } else { // Focus is on an input.
                if (event.key === "Escape") {
                    document.activeElement.blur(); // Remove input focus.
                }
            }
        });
    }, 500);

    // Illustration positioning tweaks
    let check = 0;
    let skip = 0;
    setInterval(() => { // Periodically check for updates.
        let imageContainers = $("div.column-panel.col-padding-huge").filter(function () { return $(this).find("img").length && !$(this).hasClass("anvil-role-full-height-form"); });
        if (imageContainers.length > 0) { // If we're in a story with images...
            ++check;
            let img = $(imageContainers[0]).find("img"); // This should be the illustration image.
            let textTop = Math.ceil($(".anvil-role-full-height-form > div").filter(function () { return $(this).find(img).length; }).position().top);
            let pos = Math.max(0, $("html").scrollTop() - textTop);
            let mtop = parseFloat(img.css("margin-top"));
            let diff = pos - mtop;
            let limit = Infinity;
            let turnOld = img.data("turn");
            let turnCur = $(".anvil-role-secondary-text .label-text").filter(function () { return $(this).text().includes("Turn number"); }).text(); // Get turn text.
            if (turnOld != turnCur) { // Different turn, so set the turn and clear the limit.
                img.data("turn", turnCur);
                img.removeData("limit");
                skip = 3; // Skip the next three limit update checks.
            } else {
                if (diff < -80) { // Scrolled up, so clear the limit.
                    img.removeData("limit");
                } else {
                    limit = img.data("limit") ?? Infinity;
                }
            }
            let loaded = $("i.left.fa-redo:visible").length > 0 || $("i.left.fa-floppy-o:visible").length > 0;
            if (!loaded) {
                limit = Infinity;
            }
            if ((pos <= limit || mtop < limit) && Math.abs(diff) >= 4) {
                // If (the target position isn't greater than the limit or the current top is less than the limit) and the target position is more than 4 pixels different from the current position, then update the position.
                let footer = $('[anvil-role="footer-panel"]').offset();
                img.css({
                    marginTop: Math.min(pos, limit)
                });
                if (loaded && skip-- <= 0 && diff > 0) { // If not a recently new turn and scrolled down...
                    setTimeout(() => { // ...wait to see if the footer is moved by repositioning the image.
                        if ($('[anvil-role="footer-panel"]').offset()?.top > (footer?.top ?? Infinity) + 4) { // If the footer moved down by more than 4 pixels, then we moved the image down too much.
                            let newLimit = Math.round(parseFloat(img.css("margin-top")) + footer.top - $('[anvil-role="footer-panel"]').offset().top);
                            img.data("limit", newLimit).css("margin-top", newLimit); // Set the limit and fix the position.
                        }
                    }, 100);
                }
            }
        }

        // Popup tweaks
        if ($('.modal-content a[href="https://discord.gg/eJKbKwdArY"]').length > 0) { // Close Discord popup.
            $(".modal-content .btn-success").click();
        }
    }, 300);
})();