@import suwayomi.tachidesk.i18n.MR

@param locale: java.util.Locale

<!DOCTYPE html>
<html lang="en">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content" />
    <title>${MR.strings.webview_label_title.localized(locale)}</title>
    <style>
        * {
            box-sizing: border-box;
        }
        html, body {
            margin: 0;
            padding: 0;
            height: 100%;
        }
        body {
            display: flex;
            flex-direction: column;
            font-family: "Roboto","Helvetica","Arial",sans-serif;
            font-weight: 400;
            letter-spacing: 0em;
        }
        body.disconnected::after {
            content: "${MR.strings.webview_label_disconnected.localized(locale)}";
            position: absolute;
            inset: 0;
            background: rgba(150, 0, 0, 0.5);
            color: white;
            text-align: center;
            align-content: center;
            font-size: 2rem;
        }
        button[disabled], input[disabled] {
            cursor: not-allowed;
        }
        header {
            background-color: rgb(34, 38, 53);
            box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 4px -1px, rgba(0, 0, 0, 0.14) 0px 4px 5px 0px, rgba(0, 0, 0, 0.12) 0px 1px 10px 0px;
            color: #fff;
            padding: 8px 32px;
        }
        header h1, header p {
            margin: 0;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        header nav {
            display: flex;
            flex-wrap: wrap;
            column-gap: 20px;
            align-items: center;
        }
        header form {
            display: flex;
            gap: 5px;
            flex: auto 1 1;
            min-width: 400px;
        }
        header label {
            flex: auto 0 0;
            cursor: pointer;
        }
        header button {
            all: unset;
            padding: 8px;
            border-radius: 50%;
            min-width: 1em;
            line-height: 1;
            text-align: center;
        }
        header button:not([disabled]) {
            cursor: pointer;
        }
        header button:not([disabled]):hover {
            background-color: rgba(255, 255, 255, 0.08);
        }
        header input {
            flex: 100% 1 1;
        }
        main, iframe {
            height: 100%;
        }
        main {
            position: relative;
        }
        canvas, input#inputtrap {
            position: absolute;
            inset: 0;
            width: 100%;
            height: 100%;
        }
        input#inputtrap {
            opacity: 0;
            padding: 0;
            margin: 0;
            border: none;
        }
        main .message, main .status {
            position: relative;
            z-index: 1;
        }
        main .message {
            padding: 8px;
            max-width: 1100px;
            margin: auto;
            font-style: italic;
        }
        main .message.error {
            color: red;
            font-style: regular;
            font-weight: bold;
        }
        main .message:empty {
            display: none;
        }
        main .status {
            position: absolute;
            bottom: 0;
            left: 0;
            max-width: 50%;
            background: #555;
            color: white;
            padding: 2px 4px;
            font-size: 0.8rem;
            border-top-right-radius: 3px;
        }
        main .status:empty {
            display: none;
        }
        main .contextmenu {
            display: none;
            position: absolute;
            right: 0;
            max-width: fit-content;
            min-height: calc(1.5em + 2px + 2px + 4px);
            min-width: 120px;
            flex-wrap: wrap;
            border-radius: 4px;
            border: 1px solid #333;
        }
        main .contextmenu.show {
            display: flex;
        }
        main .contextmenu button {
            all: unset;
            line-height: 1.5;
            flex-grow: 1;
            background: white;
            transition: background 0.1s ease-in-out;
            cursor: pointer;
            border: 0.5px solid #666;
            padding: 2px 4px;
            text-align: center;
        }
        main .contextmenu button:hover {
            background: #eee;
        }
        .copydialog, .logindialog {
            display: none;
            position: absolute;
            inset: 0;
            width: 100%;
            height: 100%;
            padding: 6px;
            z-index: 1;
        }
        .copydialog.show, .logindialog.show {
            display: block;
        }
        .copydialog::before, .logindialog::before {
            content: '';
            position: absolute;
            inset: 0;
            background: black;
            opacity: 0.3;
        }
        .copydialog__inner, .logindialog__inner {
            position: relative;
            max-width: 960px;
            border-radius: 8px;
            border: 1px solid #333;
            background: #eee;
            padding: 8px;
            margin: auto;
            height: 100%;
        }
        .copydialog__title {
            display: flex;
            flex-wrap: nowrap;
            justify-content: space-between;
        }
        .copydialog input {
            width: 100%;
        }
        .copydialog .close {
            flex-shrink: 1;
            all: unset;
            cursor: pointer;
            align-self: start;
            font-size: 2rem;
            line-height: 1;
        }
        @media (min-width: 500px) {
            .copydialog, .logindialog {
                padding: 24px;
            }
            .copydialog__inner, .logindialog__inner {
                padding: 12px 18px;
                height: auto;
            }
        }

        /* https://css-tricks.com/snippets/css/css-triangle/ */
        .arrow-right {
            display: inline-block;
            width: 0;
            height: 0;
            border-top: 9px solid transparent;
            border-bottom: 9px solid transparent;
            border-left: 9px solid currentcolor;
        }

        .logindialog .error {
            margin: 8px;
            padding: 8px 16px;
            border-radius: 8px;
            border: 1px solid #b71c1c;
            background-color: #c62828;
            color: white;
        }
        .logindialog .error:empty {
            display: none;
        }
        .logindialog form label {
            cursor: pointer;
        }
        .logindialog form button {
            all: unset;
            padding: 8px;
            line-height: 1.75;
            text-align: center;
            min-width: 64px;
            border-radius: 4px;
            padding: 6px 8px;
            color: rgb(91, 116, 239);
            text-transform: uppercase;
            letter-spacing: 0.02857em;
        }
        .logindialog form button:not([disabled]) {
            cursor: pointer;
        }
        .logindialog form button:not([disabled]):hover {
            background-color: rgba(91, 116, 239, 0.08);
        }
        .logindialog form input {
            all: unset;
            border-radius: 4px;
            border: 1px solid rgba(255, 255, 255, 0.23);
            padding: 6px 12px;
            width: auto;
            min-width: 0;
        }
        .logindialog form input:hover {
            border-color: white;
        }
        .logindialog form input:focus {
            border-color: rgb(91, 116, 239);
        }
        .logindialog form .controls {
            display: grid;
            align-items: center;
            grid-template-columns: 1fr;
        }
        .logindialog form .controls > :nth-child(even):not(:last-child) {
            margin-bottom: 6px;
        }
        .logindialog form .submit {
            display: flex;
            flex-direction: column;
            align-items: center;
            margin-top: 24px;
        }
        .logindialog input:disabled, .logindialog button:disabled {
            opacity: 0.7;
        }

        @media (min-width: 500px) {
            .logindialog form {
                width: 100%;
                max-width: 450px;
                margin: 8px auto;
            }
            .logindialog form .controls {
                grid-template-columns: auto 1fr;
                column-gap: 16px;
                row-gap: 6px;
            }
            .logindialog form .controls > :nth-child(even):not(:last-child) {
                margin-bottom: 0px;
            }
        }
    </style>
</head>
<body>
    <header>
        <h1 id="title">${MR.strings.webview_label_title.localized(locale)}</h1>
        <nav>
            <form id="browseForm">
                <input type="text" id="url" name="url" placeholder="${MR.strings.webview_placeholder_url.localized(locale)}" disabled/>
                <button type="submit" id="goButton" disabled><span class="arrow-right"></span></button>
            </form>
            <label><input type="checkbox" id="reverseScroll" disabled/> ${MR.strings.webview_label_reversescroll.localized(locale)}</label>
        </nav>
      <p><i>${MR.strings.webview_label_bindingshint.localized(locale)}</i></p>
    </header>
    <main>
        <div class="message" id="message">${MR.strings.webview_label_init.localized(locale)}</div>
        <div class="status" id="status"></div>
        <canvas id="frame"></canvas>
        <input type="text" id="inputtrap" autocomplete="off"/>
        <div class="contextmenu" id="contextmenu" role="menu">
            <button type="button" id="menu_copy" role="menuitem">Copy</button>
            <button type="button" id="menu_paste" role="menuitem">Paste</button>
        </div>
    </main>
    <div class="copydialog" id="copydialog" role="dialog">
        <div class="copydialog__inner">
            <div class="copydialog__title">
                <h2>${MR.strings.webview_label_copy.localized(locale)}</h2>
                <button type="button" class="close" id="copyclose" title="${MR.strings.label_close.localized(locale)}">&times;</button>
            </div>
            <p>${MR.strings.webview_label_copy_description.localized(locale)}</p>
            <input type="text" id="copyinput" disabled readonly/>
        </div>
    </div>
    <div class="logindialog" id="logindialog" role="dialog">
        <div class="logindialog__inner">
            <form>
                <h2>Login</h2>
                <div class="error"></div>
                <p>${MR.strings.webview_label_login_required.localized(locale)}</p>
                <div class="controls">
                    <label for="user">${MR.strings.login_label_username.localized(locale)}:</label>
                    <input type="text" name="user" id="user" required placeholder="${MR.strings.login_placeholder_username.localized(locale)}"/>
                    <label for="pass">${MR.strings.login_label_password.localized(locale)}:</label>
                    <input type="password" name="pass" id="pass" required placeholder="${MR.strings.login_placeholder_password.localized(locale)}"/>
                </div>
                <div class="submit">
                    <button type="submit" disabled>${MR.strings.login_label_login.localized(locale)}</button>
                </div>
            </form>
        </div>
    </div>
    <script>
        const messageDiv = document.getElementById('message');
        const statusDiv = document.getElementById('status');
        const copyDiv = document.getElementById('copydialog');
        const copyInput = document.getElementById('copyinput');
        const copyClose = document.getElementById('copyclose');
        const contextMenuDiv = document.getElementById('contextmenu');
        const contextMenuCopy = document.getElementById('menu_copy');
        const contextMenuPaste = document.getElementById('menu_paste');
        const frame = document.getElementById('frame');
        const frameInput = document.getElementById('inputtrap');
        const ctx = frame.getContext("2d");
        const browseForm = document.getElementById('browseForm');
        const goButton = document.getElementById('goButton');
        const urlInput = document.getElementById('url');
        const titleDiv = document.getElementById('title');
        const reverseToggle = document.getElementById('reverseScroll');
        const origTitle = document.title;
        const params = new URLSearchParams(window.location.search);
        const token = params.get('token');

        function connectWs(socketUrl, token) {
            return new Promise((resolve, reject) => {
                // we pass the token as the subprotocol, which is widely considered the best solution to passing tokens
                // browsers don't support setting custom headers for WebSockets...
                const socket = new WebSocket(socketUrl, token ? [token] : []);
                const f = (msg) => {
                    console.debug('Connection active:', msg.data);
                    socket.removeEventListener('message', f);
                    socket.removeEventListener('close', closef);
                    resolve(socket);
                };
                const closef = (e) => {
                    socket.removeEventListener('message', f);
                    if (e.code === 1011 && e.reason === "Unauthorized") {
                        const loginDiv = document.getElementById('logindialog');
                        const loginForm = document.querySelector('#logindialog form');
                        const loginError = document.querySelector('#logindialog .error');
                        loginError.textContent = '';
                        loginForm.querySelectorAll('input, button').forEach(i => i.disabled = false);
                        loginForm.addEventListener('submit', async (sev) => {
                            sev.preventDefault();
                            loginForm.querySelectorAll('input, button').forEach(i => i.disabled = true);
                            const mutation = {
                                "query": "mutation LOGIN($input: LoginInput!) {\n  login(input: $input) {\n    accessToken\n    refreshToken\n  }\n}",
                                "variables": {
                                    "input": {
                                        "username": loginForm.user.value,
                                        "password": loginForm.pass.value,
                                    },
                                },
                                "operationName": "LOGIN",
                            };
                            const resp = await fetch("/api/graphql", {
                                "headers": {
                                    "Accept": "application/json, multipart/mixed",
                                    "Content-Type": "application/json",
                                    "Cache-Control": "no-cache"
                                },
                                "body": JSON.stringify(mutation),
                                "method": "POST",
                            }).then(r => r.json());
                            if (resp.errors && resp.errors.length > 0) {
                                const err = resp.errors[0].message.replace(/Exception[^:]* :|\r?\n.*/g, '');
                                loginError.textContent = err;
                                loginForm.pass.value = '';
                                loginForm.querySelectorAll('input, button').forEach(i => i.disabled = false);
                            } else {
                                const newToken = resp.data.login.accessToken;
                                const expiry = new Date(JSON.parse(atob(newToken.split('.')[1])).exp * 1000);
                                console.log('Got new token', newToken, 'expires', expiry);
                                document.cookie = "suwayomi-server-token=" + newToken + "; path=/; expires=" + expiry.toUTCString();
                                loginDiv.classList.remove('show');
                                loginForm.querySelectorAll('input, button').forEach(i => i.disabled = true);
                                const newSocket = new WebSocket(socketUrl, [newToken]);
                                newSocket.addEventListener('open', () => {
                                    resolve(newSocket);
                                });
                            }
                        });
                        loginDiv.classList.add('show');
                        return;
                    }
                    socket.removeEventListener('close', closef);
                    reject(e);
                };
                socket.addEventListener('open', () => {
                    console.debug('Socket opened, PING');
                    socket.addEventListener('message', f);
                    socket.send(JSON.stringify({type: "ping"}));
                });
                socket.addEventListener('close', closef);
            });
        }

        (async function() {
            try {
                const socketUrl = (window.location.origin + window.location.pathname).replace(/^http/,'ws');
                // we pass the token as the subprotocol, which is widely considered the best solution to passing tokens
                // browsers don't support setting custom headers for WebSockets...
                const socket = await connectWs(socketUrl, token);

                urlInput.disabled = false;
                goButton.disabled = false;
                reverseToggle.disabled = false;
                reverseToggle.checked = window.localStorage.getItem('suwayomi_mouse_reverse_scroll') === "true";

                let url = '';
                try {
                    url = window.decodeURIComponent(window.location.hash.replace(/^#/, ''));
                } catch (e) {
                    console.error(e);
                }

                copyClose.addEventListener('click', () => {
                    copyDiv.classList.remove('show');
                });

                contextMenuCopy.addEventListener('click', () => {
                    socket.send(JSON.stringify({
                        type: 'copy',
                    }));
                    contextMenuDiv.classList.remove('show');
                });

                contextMenuPaste.addEventListener('click', () => {
                    navigator.clipboard.readText().then(data => {
                        socket.send(JSON.stringify({
                            type: 'paste',
                            data: data,
                        }));
                    });
                    contextMenuDiv.classList.remove('show');
                });

                if (!navigator.clipboard || !navigator.clipboard.readText) {
                    // if not served via HTTPS, remove the button, users can still paste via clipboard
                    // e.g. Ctrl+V or the dedicated paste button on gboard
                    // TODO: dialog like with copy?
                    contextMenuPaste.remove();
                }

                /// Helpers

                const setHash = (u) => {
                    let current = '';
                    try {
                        current = window.decodeURIComponent(window.location.hash.replace(/^#/, ''));
                    } catch (e) {
                        console.error(e);
                    }
                    if (current != u)
                        history.pushState(null, null, window.location.origin + window.location.pathname + '#' + window.encodeURIComponent(u));
                };

                const setTitle = (title) => {
                    if (!title) {
                        document.title = origTitle;
                        titleDiv.textContent = origTitle;
                    } else {
                        document.title = "Suwayomi: " + title;
                        titleDiv.textContent = "Suwayomi: " + title;
                    }
                }

                const loadUrl = (u) => {
                    if (!u) {
                        urlInput.value = u;
                        setHash(u);
                        setTitle();
                        messageDiv.textContent = "${MR.strings.webview_label_getstarted.localized(locale)}";
                        ctx.clearRect(0, 0, frame.width, frame.height);
                        return;
                    }
                    messageDiv.textContent = "${MR.strings.webview_label_loading.localized(locale)}";
                    messageDiv.classList.remove('error');
                    urlInput.value = u;
                    socket.send(JSON.stringify({ type: 'loadUrl', url: u, width: frame.clientWidth, height: frame.clientHeight }));
                    ctx.clearRect(0, 0, frame.width, frame.height);
                };

                const copy = (data) => {
                    try {
                        if (!!navigator.clipboard && !!navigator.clipboard.writeText) {
                            navigator.clipboard.writeText(data);
                            return;
                        }
                        console.warn('Clipbaord API not supported (not served over HTTPS?), presenting dialog');
                    } catch (e) {
                        console.error('Clipboard access threw, presenting dialog', e);
                    }
                    copyInput.value = data;
                    copyDiv.classList.add('show');
                }

                /// Form

                window.addEventListener('hashchange', e => {
                    const url = window.decodeURIComponent(window.location.hash.replace(/^#/, ''));
                    loadUrl(url);
                    console.log('Navigate to', url);
                });

                browseForm.addEventListener('submit', e => {
                    e.preventDefault();
                    const url = urlInput.value;
                    loadUrl(url);
                    console.log('Navigate to', url);
                });

                reverseToggle.addEventListener('change', e => {
                    window.localStorage.setItem('suwayomi_mouse_reverse_scroll', e.target.checked ? "true" : "false");
                });

                /// Server events

                socket.addEventListener('message', e => {
                    const obj = JSON.parse(e.data);
                    switch (obj.type) {
                        case "addressChange":
                            console.log('Loaded');
                            messageDiv.textContent = '';
                            urlInput.value = obj.url;
                            setHash(obj.url);
                            setTitle(obj.title);
                            break;
                        case "statusChange":
                            statusDiv.textContent = obj.message;
                            break;
                        case "load": {
                            if (obj.error) {
                                messageDiv.textContent = "${MR.strings.label_error.localized(locale)}: " + obj.error;
                                messageDiv.classList.add('error');
                            } else {
                                messageDiv.textContent = "";
                            }
                            urlInput.value = obj.url;
                            setTitle(obj.title);
                        } break;
                        case "render": {
                            const img = new Image();
                            const imgData = new Blob([new Uint8Array(obj.image)], { type: "image/png" });
                            const url = URL.createObjectURL(imgData);
                            img.addEventListener('load', e => {
                                frame.width = img.width;
                                frame.height = img.height;
                                ctx.drawImage(img, 0, 0);
                            });
                            img.src = url;
                        } break;
                        case "consoleMessage": {
                            const lg = obj.severity == 4 ? console.error : obj.severity == 3 ? console.warn : console.log;
                            lg(obj.source + ':' + obj.line + ':', obj.message);
                        } break;
                        case "copy":
                            copy(obj.content);
                            break;
                        default:
                            console.warn("Unknown event", obj.type)
                            break;
                    }
                });

                socket.addEventListener('close', e => {
                    if (e.wasClean) {
                        console.log(`WebSocket connection closed cleanly, code=` + e.code + `, reason=` + e.reason);
                    } else {
                        console.error('WebSocket connection died', e);
                    }
                    document.body.classList.add('disconnected');
                });

                socket.addEventListener('error', e => {
                    messageDiv.textContent = "${MR.strings.label_error.localized(locale)}: " + (e.message || e.reason || e);
                    messageDiv.classList.add('error');
                    console.error('WebSocket error:', e);
                });

                /// Page events

                const observer = new ResizeObserver(() => {
                    socket.send(JSON.stringify({ type: 'resize', width: frame.clientWidth, height: frame.clientHeight }));
                });
                observer.observe(frame);

                const frameEvent = (e) => {
                    // Chrome Android bug, see input below
                    if (e.key === "Unidentified") return;
                    // paste is handled in input below
                    if (e.key === "v" && e.ctrlKey === true) return;
                    if (e.key === "c" && e.ctrlKey === true) {
                        if (e.type === "keydown") {
                            socket.send(JSON.stringify({
                                type: 'copy',
                            }));
                        }
                        return;
                    }
                    e.preventDefault();
                    if (e.type === "mousedown" && contextMenuDiv.classList.contains('show')) {
                        console.log('remove context menu');
                        contextMenuDiv.classList.remove('show');
                        return;
                    }
                    // right-click, handled in contextmenu below
                    if (e.type === "mousedown" && e.button === 2) return;
                    const rect = frame.getBoundingClientRect();
                    const clickX = e.clientX !== undefined ? e.clientX - rect.left : 0;
                    const clickY = e.clientY !== undefined ? e.clientY - rect.top : 0;
                    socket.send(JSON.stringify({
                        type: 'event',
                        eventType: e.type,
                        clickX,
                        clickY,
                        button: e.button,
                        ctrlKey: e.ctrlKey,
                        shiftKey: e.shiftKey,
                        altKey: e.altKey,
                        metaKey: e.metaKey,
                        key: e.key,
                        clientX: e.clientX,
                        clientY: e.clientY,
                        deltaY: reverseToggle.checked && typeof e.deltaY === 'number' ? -e.deltaY : e.deltaY,
                    }));
                    frameInput.focus();
                };

                const attachEvents = () => {
                    console.log('Attaching event handlers to new document');
                    const events = ["mousedown", "mouseup", "mousemove", "wheel", "keydown", "keyup"];
                    for (const ev of events) {
                        frameInput.addEventListener(ev, frameEvent, false);
                    }

                    let touch = undefined;
                    frameInput.addEventListener('touchstart', e => {
                        if (e.touches.length === 1) {
                            touch = e.touches[0];
                        }
                    }, false);
                    frameInput.addEventListener('touchend', e => {
                        touch = undefined;
                    }, false);
                    frameInput.addEventListener('touchmove', e => {
                        if (e.touches.length === 1 && touch !== undefined) {
                            e.preventDefault();
                            let deltaX = touch.pageX - e.touches[0].pageX;
                            let deltaY = touch.pageY - e.touches[0].pageY;
                            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                                // assume horizontal scroll
                                socket.send(JSON.stringify({
                                    type: 'event',
                                    eventType: 'wheel',
                                    clickX: e.touches[0].pageX,
                                    clickY: e.touches[0].pageY,
                                    shiftKey: true,
                                    clientX: e.touches[0].clientX,
                                    clientY: e.touches[0].clientY,
                                    deltaY: deltaX,
                                }));
                            } else {
                                socket.send(JSON.stringify({
                                    type: 'event',
                                    eventType: 'wheel',
                                    clickX: e.touches[0].pageX,
                                    clickY: e.touches[0].pageY,
                                    clientX: e.touches[0].clientX,
                                    clientY: e.touches[0].clientY,
                                    deltaY: deltaY,
                                }));
                            }
                            touch = e.touches[0];
                        }
                    }, false);
                    // known bug on Chrome Android:
                    // https://stackoverflow.com/questions/36753548/keycode-on-android-is-always-229
                    // on other browsers, the preventDefault above works so we don't get this event
                    frameInput.addEventListener('input', e => {
                        e.preventDefault();
                        socket.send(JSON.stringify({
                            type: 'paste',
                            data: e.data,
                        }));
                        e.target.value = '';
                    });
                    frameInput.addEventListener('contextmenu', e => {
                        e.preventDefault();
                        contextMenuDiv.style.left = e.offsetX + 'px';
                        contextMenuDiv.style.top = e.offsetY + 'px';
                        contextMenuDiv.classList.add('show');

                        const shiftLeft = contextMenuDiv.offsetParent.offsetWidth - contextMenuDiv.offsetWidth - contextMenuDiv.offsetLeft;
                        if (shiftLeft < 0)
                            contextMenuDiv.style.left = (e.offsetX + shiftLeft) + 'px';
                        const shiftTop = contextMenuDiv.offsetParent.offsetHeight - contextMenuDiv.offsetHeight - contextMenuDiv.offsetTop;
                        if (shiftTop < 0)
                            contextMenuDiv.style.top = (e.offsetY + shiftTop) + 'px';
                    }, false);
                };
                attachEvents();
                frameInput.focus();
                loadUrl(url);
            } catch (e) {
                messageDiv.textContent = "${MR.strings.label_error.localized(locale)}: " + (e.message || e.reason || e);
                messageDiv.classList.add('error');
                console.error(e);
            }
        })();
    </script>
</body>
</html>
