mai-notes++

1/26/2026, 8:58:23 PM

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name        mai-notes++
// @namespace   Violentmonkey Scripts
// @match       https://mai-notes.com/player.html*
// @version     1.0
// @author      Dylan Dang
// @description 1/26/2026, 8:58:23 PM
// @require     https://unpkg.com/lighterhtml
// @require     https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4
// @grant       GM_setValue
// @grant       GM_getValue
// @run-at      document-start
// @license MIT
// ==/UserScript==

function afterHook(target, methodName, after) {
    const originalMethod = target[methodName];
    target[methodName] = function (...args) {
        const returnValue = originalMethod.apply(this, args);
        after.apply(this, [args, returnValue]);
        return returnValue;
    }
}

const scriptTagObserver = new MutationObserver((mutations) => {
    mutations.forEach(mutation => {
        mutation.addedNodes.forEach(async node => {
            if (node.tagName === "SCRIPT") {
                if (node.src.includes("/assets/player")) {
                    // Remove the module tag to make variables global
                    node.type = "";
                }
            }
        });
    });
});
scriptTagObserver.observe(document.documentElement, { childList: true, subtree: true });


async function ready() {
    return new Promise((resolve) => {
        let interval;
        function check() {
            if (!Wi || !Gi) return false;
            resolve({ player: Wi, audioManager: Gi });
            clearInterval(interval);
            return true;
        }
        if (check()) return;
        interval = setInterval(check, 30);
    });
}

const { html: { node: html } } = lighterhtml;

async function loadMaterialSymbolsFont() {
    return new Promise((resolve) => {
        document.head.appendChild(html`
        <link rel="stylesheet"
              onload=${resolve} 
              href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" />
        `);
    });
}

/**
 * Wait for an element to appear in the DOM.
 * @param {string} selector - The CSS selector to watch for.
 * @param {Node} parent - The container to watch (defaults to document).
 */
async function getElement(selector, parent = document) {
    return new Promise((resolve) => {
        const existingElement = parent.querySelector(selector);
        if (existingElement) {
            return resolve(existingElement);
        }
        const observer = new MutationObserver((mutations) => {
            const target = parent.querySelector(selector);
            if (target) {
                resolve(target);
                observer.disconnect();
            }
        });
        observer.observe(parent === document ? document.documentElement : parent, {
            childList: true,
            subtree: true,
        });
    });
}

function handleFullscreenChange() {
    const canvas = document.getElementById("chartCanvas");
    if (document.fullscreenElement === canvas) {
        canvas.parentElement.style.maxWidth = "100%";
    } else {
        canvas.parentElement.removeAttribute("style");
    }
    window.dispatchEvent(new Event('resize'));
}

const fullScreenChangeEvents = [
    'fullscreenchange',
    'webkitfullscreenchange',
    'mozfullscreenchange',
    'MSFullscreenChange'
];

fullScreenChangeEvents.forEach(eventType => {
    document.addEventListener(eventType, handleFullscreenChange);
});

function fullscreen() {
    const canvas = document.getElementById("chartCanvas");
    canvas.parentElement.style.maxWidth = "100%";
    if (canvas.requestFullScreen) {
        canvas.requestFullScreen();
    } else if (canvas.webkitRequestFullScreen) {
        canvas.webkitRequestFullScreen();
    } else if (canvas.mozRequestFullScreen) {
        canvas.mozRequestFullScreen();
    }
}
document.addEventListener('keydown', function (e) {
    const active = document.activeElement;
    const notEditing = !(active && (
        active.tagName === 'INPUT' ||
        active.tagName === 'TEXTAREA' ||
        active.isContentEditable
    ));
    if (e.code === 'Space' && notEditing) {
        e.preventDefault();
        document.getElementById('playPauseButton')?.click();
    }

    if ((e.code === 'ArrowRight' || e.key === 'ArrowRight') && notEditing) {
        if (e.shiftKey) {
            e.preventDefault();
            document.getElementById('measure5ForwardButton')?.click();
        } else if (e.ctrlKey) {
            e.preventDefault();
            document.getElementById('measureForwardButton')?.click();
        } else {
            e.preventDefault();
            document.getElementById('positionForwardButton')?.click();
        }
    }

    if ((e.code === 'ArrowLeft' || e.key === 'ArrowLeft') && notEditing) {
        if (e.shiftKey) {
            e.preventDefault();
            document.getElementById('measure5BackButton')?.click();
        } else if (e.ctrlKey) {
            e.preventDefault();
            document.getElementById('measureBackButton')?.click();
        } else {
            e.preventDefault();
            document.getElementById('positionBackButton')?.click();
        }
    }

    if ((e.code === 'KeyF' || e.key === 'f' || e.key === 'F') && notEditing) {
        e.preventDefault();
        if (
            document.fullscreenElement ||
            document.webkitFullscreenElement ||
            document.mozFullScreenElement
        ) {
            if (document.exitFullscreen) {
                document.exitFullscreen();
            } else if (document.webkitExitFullscreen) {
                document.webkitExitFullscreen();
            } else if (document.mozCancelFullScreen) {
                document.mozCancelFullScreen();
            }
        } else {
            fullscreen();
        }
    }

    if ((e.code === 'KeyR' || e.key === 'r' || e.key === 'R') && notEditing) {
        e.preventDefault();
        document.getElementById('restartButton')?.click();
    }
});

document.addEventListener("wheel", function (e) {
    if (
        document.fullscreenElement ||
        document.webkitFullscreenElement ||
        document.mozFullScreenElement
    ) {
        if (e.deltaY < 0) {
            if (e.shiftKey) {
                document.getElementById('measure5BackButton')?.click();
            } else if (e.ctrlKey) {
                document.getElementById('measureBackButton')?.click();
            } else {
                document.getElementById('positionBackButton')?.click();
            }
        } else if (e.deltaY > 0) {
            if (e.shiftKey) {
                document.getElementById('measure5ForwardButton')?.click();
            } else if (e.ctrlKey) {
                document.getElementById('measureForwardButton')?.click();
            } else {
                document.getElementById('positionForwardButton')?.click();
            }
        }
    }
}, { passive: true });

async function reInitializeAudioManager() {
    const { audioManager } = await ready();
    audioManager.audioContext = new (
        window.AudioContext || window.webkitAudioContext
    )();
    const response = await fetch("https://sdez.zip/voices/system/jp/circle/fx/SE_GAME_ANSWER.mp3");
    const arrayBuffer = await response.arrayBuffer();
    audioManager.answerBuffer = await audioManager.audioContext.decodeAudioData(arrayBuffer);
    audioManager.initialized = true;
}

async function injectAudioUploadUI() {
    const preview = html`<audio class="w-full mt-3 rounded" controls style="display:none"
    onseeking=${() => {
            if (!Wi) return;
            const trueTime = Wi.Gt(Wi.timeline.te()) / 1000;
            if (Wi.playing && Math.abs(preview.currentTime - trueTime) > 0.01) {
                preview.currentTime = trueTime;
            }
        }}/>`;

    const info = html`<div class="mt-3 text-gray-500 text-sm" />`;


    function handleFile(file) {
        if (!file.type.startsWith('audio/')) {
            info.textContent = 'Please select an audio file.';
            preview.style.display = 'none';
            preview.src = '';
            return;
        }
        info.textContent = `Selected: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)`;
        const url = URL.createObjectURL(file);
        preview.src = url;
        preview.style.display = '';
    }

    let dragCounter = 0;
    (await getElement(".controls-section")).after(html`
        <h2 style="font-size: 1.3rem;">Sync Local Audio</h2>
        <div 
            id="mai-notes-upload-area"
            class="flex flex-col items-center justify-center border-2 border-dashed border-gray-400 rounded-lg !p-8 !mt-4 bg-opacity-80 shadow transition-colors duration-200 hover:border-blue-500 select-none cursor-pointer transition-background-color"
            style="min-width:280px; max-width:800px; margin:auto;"
            onclick=${e => {
            const input = e.currentTarget.querySelector('input[type="file"]');
            if (e.target !== input) input.click();
        }}
            ondragenter=${e => {
            e.preventDefault();
            dragCounter++;
            e.currentTarget.classList.add('bg-blue-100');
        }}
            ondragover=${e => {
            e.preventDefault();
            e.currentTarget.classList.add('bg-blue-100');
        }}
            ondragleave=${e => {
            e.preventDefault();
            dragCounter--;
            if (dragCounter <= 0) {
                e.currentTarget.classList.remove('bg-blue-100');
                dragCounter = 0;
            }
        }}
            ondrop=${e => {
            e.preventDefault();
            dragCounter = 0;
            e.currentTarget.classList.remove('bg-blue-100');
            const files = e.dataTransfer.files;
            if (files.length > 0) handleFile(files[0]);
        }}
        >
            <div style="width:100%" class="flex flex-col items-center text-center">
                <span class="material-symbols-outlined text-5xl text-blue-400 mb-2">upload_file</span>
                <span class="font-semibold text-gray-700 mb-2">Drag & drop your audio file or <span class="text-blue-500">browse file</span></span>
                
                <input 
                    type="file" 
                    id="audio-upload-input" 
                    accept="audio/*"
                    class="hidden"
                    onchange=${e => {
            const files = e.target.files;
            if (files.length > 0) handleFile(files[0]);
        }}
                >
            </div>
            ${info}
            ${preview}
        </div>
    `);

    return preview;
}


async function loadSettings() {
    const { audioManager } = await ready();
    const answerSoundEnabledElement = document.getElementById("answerSoundEnabled");
    answerSoundEnabledElement.addEventListener("change", (e) => {
        e.preventDefault();
        GM_setValue("answerSoundEnabled", e.target.checked);
    });
    const answerSoundEnabled = GM_getValue("answerSoundEnabled", true);
    answerSoundEnabledElement.checked = answerSoundEnabled;
    audioManager.be(answerSoundEnabled);
}

async function main() {
    await loadMaterialSymbolsFont();
    const playButton = await getElement("#playPauseButton");
    playButton.after(html`<button id="restartButton" class="control-button material-symbols-outlined !leading-normal" onclick=${fullscreen}>crop_free</button>`);

    await loadSettings();
    const audio = await injectAudioUploadUI();
    await reInitializeAudioManager();
    const { player } = await ready();

    document.querySelector("#playbackSpeedSlider").addEventListener("input", (e) => {
        audio.playbackRate = e.target.value;
    });

    function setAudioCurrentTime() {
        audio.currentTime = Wi.playbackStartPositionMs / 1000;
    }

    const playerPrototype = Object.getPrototypeOf(player);

    afterHook(playerPrototype, "re", () => {
        setAudioCurrentTime();
        audio.play();
    });
    afterHook(playerPrototype, "oe", setAudioCurrentTime);


    afterHook(playerPrototype, "ae", () => {
        audio.pause();
    });

}

main();