Greasy Fork

Greasy Fork is available in English.

A岛引用查看增强

让A岛网页端的引用支持嵌套查看、固定、折叠等功能

当前为 2021-03-22 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        A岛引用查看增强
// @namespace   http://tampermonkey.net/
// @version     0.1.16.1
// @description 让A岛网页端的引用支持嵌套查看、固定、折叠等功能
// @author      FToovvr
// @license     MIT; https://opensource.org/licenses/MIT
// @include     /^https?://(adnmb\d*.com|tnmb.org)/.*$/
// @grant       none
// ==/UserScript==

// TODO T1:
// TODO: cache 先占个位,减小重复请求可能性
// TODO: 持久化缓存;配置可选数量;按「最后看到的时间」、「是直接看到还是获取引用看到」决定权重
// TODO: 如果强制重新加载引用,是不是该同步刷新其他加载了相同引用内容的 view?reactivity?
// TODO: 自动固定其他相同的引用?
// TODO T2:
// TODO: 自定义配置页 https://stackoverflow.com/a/43462416
// TODO: 配置决定:点图钉是悬浮还是关闭?正常载入是否还提供刷新按钮?多少秒算超时?是否自动固定存在缓存的引用内容?
// TODO T3:
// TODO: 随时有图钉按钮解除固定?
// TODO: 🚫 来直接关闭,放在最右侧?
// TODO: 要不要考虑尝试在重新加载后还原先前展开/折叠的状态?
// TODO: 悬浮淡出
// TODO: 折叠时点击 mask,preventDefault?
// TODO T?:
// TODO?: 优化引用内容的空白?

// 测试场地:
// * 长内容: https://adnmb3.com/t/36053697
// * 长内容2: https://adnmb3.com/t/36048637?page=2
// * 不存在内容: https://adnmb3.com/f/值班室
// * 同一份不存在的内容两次:https://adnmb3.com/t/36081863
// * 超级嵌套: https://adnmb3.com/t/20311039?page=1641
// * 各种内容: https://adnmb3.com/t/26165309?page=3(页数是随便选的)

(function () {
    'use strict';

    // TODO: 配置决定
    const collapsedHeight = 80;
    const floatingOpacity = '100%'; // '90%';
    const fadingDuration = 0; // '80ms';
    const clickPinToCloseView = false;
    const refFetchingTimeout = 10000; // 10 秒
    const showRefreshButtonEvenIfRefContentLoaded = false;
    const autoOpenRefWhenCached = false;

    const additionalStyleText = `

    .h-threads-content {
        word-break: break-word;
    }
    
    .fto-ref-view {
        /* 照搬自 h.desktop.css '#h-ref-view .h-threads-item-ref' */
        background: #f0e0d6;
        border: 1px solid #000;
        clear: left;
    
        position: relative;
    
        width: max-content;
        max-width: calc(100vw - var(--offset-left) - 35px);
    
        margin-left: -5px;
        margin-right: -40px;
    }
    
    .h-threads-item-ref .h-threads-content {
        margin: 5px 20px;
    }
    
    /* 修复 h.desktop.css 里 '.h-threads-item .h-threads-content' 这条选择器导致的问题 */
    .h-threads-info {
        font-size: 14px;
        line-height: 20px;
        margin: 0px;
    }
    
    .fto-ref-view[data-status="closed"] {
        /* display: none; */
        opacity: 0; display: inline-block;
        width: 0; height: 0; overflow: hidden;
        padding: 0; border: 0; margin: 0;
    
        /* transition: opacity ${fadingDuration} ease-out; */
    }
    
    .fto-ref-view[data-status="floating"] {
        position: absolute;
        z-index: 999;
    
        opacity: ${floatingOpacity};
    
        transition: opacity ${fadingDuration} ease-in;
    }
    
    .fto-ref-view[data-status="open"] {
        display: block;
    }
    .fto-ref-view[data-status="open"] + br {
        display: none;
    }
    
    .fto-ref-view[data-status="collapsed"] {
        display: block;
        max-height: ${collapsedHeight}px;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    .fto-ref-view[data-status="collapsed"] + br {
        display: none;
    }
    
    /* https://stackoverflow.com/a/22809380 */
    .fto-ref-view[data-status="collapsed"]:before {
        content: '';
        position: absolute;
        top: 60px;
        height: 20px;
        width: 100%;
        background: linear-gradient(#f0e0d600, #ffeeddcc);
        z-index: 999;
    }
    
    .fto-ref-view-button {
        position: relative;
    
        font-size: smaller;
    
        cursor: pointer;
        -webkit-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;
    }
    
    .fto-ref-view-pin {
        display: inline-block;
        transform: rotate(-45deg);
    }
    /* https://codemyui.com/grayscale-emoji-using-css/ */
    .fto-ref-view[data-status="floating"]
    >.h-threads-item >.h-threads-item-ref
    >.h-threads-item-reply-main >.h-threads-info
    >.fto-ref-view-button-list > .fto-ref-view-pin,
    .fto-ref-view[data-status="floating"] >.fto-ref-view-error
    >.fto-ref-view-button-list >.fto-ref-view-pin,
    .fto-ref-view[data-status="floating"] >.fto-ref-view-loading
    >.fto-ref-view-button-list >.fto-ref-view-pin {
        transform: none;
        filter: grayscale(100%);
    }
    .fto-ref-view[data-status="collapsed"]
    >.h-threads-item >.h-threads-item-ref
    >.h-threads-item-reply-main >.h-threads-info
    >.fto-ref-view-button-list >.fto-ref-view-pin:before,
    .fto-ref-view[data-status="collapsed"] >.fto-ref-view-error
    >.fto-ref-view-button-list >.fto-ref-view-pin:before,
    .fto-ref-view[data-status="collapsed"] >.fto-ref-view-loading
    >.fto-ref-view-button-list >.fto-ref-view-pin:before  {
        content: '';
        position: absolute;
        height: 110%;
        width: 100%;
        background: linear-gradient(#f0e0d600, #f0e0d6ff);
        z-index: 999;
        transform: rotate(45deg);
    }
    
    .fto-ref-view-error {
        color: red;
    }
    
    `;

    function entry() {

        if (window.disableAdnmbReferenceViewerEnhancementUserScript) {
            console.log("「A岛引用查看增强」用户脚本被禁用(设有变量 `window.disableAdnmbReferenceViewerEnhancementUserScript`),将终止。");
            return;
        }

        const model = new Model();
        if (!model.isSupported) {
            console.log("浏览器功能不支持「A岛引用查看增强」用户脚本,将终止。");
            return;
        }

        // 销掉原先的预览方法
        document.querySelectorAll('font[color="#789922"]').forEach((elem) => {
            if (elem.textContent.startsWith('>>')) {
                const newElem = elem.cloneNode(true);
                elem.parentElement.replaceChild(newElem, elem);
            }
        });

        Controller.setupStyle();

        const controller = new Controller(model);
        controller.setupContent(document.body);
    }

    class Controller {

        /**
         * 
         * @param {Model} model 
         */
        constructor(model) {
            this.model = model;
        }

        static setupStyle() {
            const style = document.createElement('style');
            style.id = 'fto-additional-style';
            // TODO: fade out
            style.append(additionalStyleText);
            document.head.append(style);
        }

        /**
         * 
         * @param {HTMLElement} root
         */
        setupContent(root) {

            if (root === document.body) {
                root.querySelectorAll('.h-threads-item').forEach((threadItemElem) => {
                    this.setupThreadContent(threadItemElem);
                });
            } else if (ViewHelper.hasFetchingRefSucceeded(root)) {
                const repliesElem = root.closest('.h-threads-item-replys');
                let threadElem;
                if (repliesElem) { // 在串的回应中
                    threadElem = repliesElem.closest('.h-threads-item');
                } else { // 在串首中
                    threadElem = root.closest('.h-threads-item-main');
                }
                const threadID = ViewHelper.getThreadID(threadElem);
                const po = ViewHelper.getPosterID(threadElem);
                this.setupRefContent(root, threadID, po);
            } else {
                this.setupErrorRefContent(root);
                return;
            }

            root.querySelectorAll('font[color="#789922"]').forEach(linkElem => {
                if (!linkElem.textContent.startsWith('>>')) { return; }
                this.setupRefLink(linkElem);
            });
        }

        /**
         * 
         * @param {HTMLElement} threadItemElem 
         */
        setupThreadContent(threadItemElem) {
            const threadID = ViewHelper.getThreadID(threadItemElem);

            { // 将串首加入缓存
                const originalItemMainElem = threadItemElem.querySelector('.h-threads-item-main');
                const itemDiv = document.createElement('div');
                itemDiv.classList.add('h-threads-item');
                const itemRefDiv = document.createElement('div');
                itemRefDiv.classList.add('h-threads-item-reply', 'h-threads-item-ref');
                itemDiv.append(itemRefDiv);
                const itemMainDiv = originalItemMainElem.cloneNode(true);
                itemMainDiv.className = '';
                itemMainDiv.classList.add('h-threads-item-reply-main');
                itemRefDiv.append(itemMainDiv);
                const infoDiv = itemMainDiv.querySelector('.h-threads-info');
                try { // 尝试修正几个按钮的位置。以后如果A岛自己修正了这里就会抛异常
                    const messedUpDiv = infoDiv.querySelector('.h-admin-tool').closest('.h-threads-info-report-btn');
                    if (!messedUpDiv) { // 版块页面里的各个按钮没搞砸
                        infoDiv.querySelectorAll('.h-threads-info-report-btn a').forEach((aElem) => {
                            if (aElem.textContent !== "举报") {
                                aElem.closest('.h-threads-info-report-btn').remove();
                            }
                        });
                        infoDiv.querySelector('.h-threads-info-reply-btn').remove();
                    } else { // 串内容页面的各个按钮搞砸了
                        infoDiv.append(
                            '', messedUpDiv.querySelector('.h-threads-info-id'),
                            '', messedUpDiv.querySelector('.h-admin-tool'),
                        );
                        messedUpDiv.remove();
                    }
                } catch (e) {
                    console.log(e);
                }
                this.model.recordRef(threadID, itemDiv, 'global');
            }

            // 将各回应加入缓存
            threadItemElem.querySelectorAll('.h-threads-item-replys .h-threads-item-reply').forEach((originalItemElem) => {
                const div = document.createElement('div');
                div.classList.add('h-threads-item');
                const itemElem = originalItemElem.cloneNode(true);
                itemElem.classList.add('h-threads-item-ref');
                itemElem.querySelector('.h-threads-item-reply-icon').remove();
                for (const child of itemElem.querySelector('.h-threads-item-reply-main').children) {
                    if (!child.classList.contains('h-threads-info')
                        && !child.classList.contains('h-threads-content')) {
                        child.remove();
                    }
                }
                itemElem.querySelectorAll('.uk-text-primary').forEach((labelElem) => {
                    if (labelElem.textContent === "(PO主)") {
                        labelElem.remove();
                    }
                });
                div.append(itemElem);
                this.model.recordRef(ViewHelper.getPostID(itemElem), div, 'global');
            });
        }

        /**
         * 
         * @param {HTMLElement} elem 
         * @param {number} threadID
         * @param {String} po
         */
        setupRefContent(elem, threadID, po) {
            const infoElem = elem.querySelector('.h-threads-info');

            // 补标 PO
            if (ViewHelper.getPosterID(infoElem) === po) {
                const poLabel = document.createElement('span');
                poLabel.textContent = "(PO主)";
                poLabel.classList.add('uk-text-primary', 'uk-text-small', 'fto-po-label');
                const elem = infoElem.querySelector('.h-threads-info-uid');
                Utils.insertAfter(elem, poLabel);
                Utils.insertAfter(elem, document.createTextNode(' '));
            }

            // 标「外串」
            if (ViewHelper.getThreadID(infoElem) !== threadID) {
                const outerThreadLabel = document.createElement('span');
                outerThreadLabel.textContent = "(外串)";
                outerThreadLabel.classList.add('uk-text-secondary', 'uk-text-small', 'fto-outer-thread-label');
                const elem = infoElem.querySelector('.h-threads-info-id');
                elem.append(' ', outerThreadLabel);
            }

            this.setupButtons(infoElem);
        }

        /**
         * 
         * @param {Element} elem 
         */
        setupErrorRefContent(elem) {
            this.setupButtons(elem);
        }

        /**
         * 
         * @param {HTMLElement} elem
         */
        setupButtons(elem) {
            const viewDiv = elem.closest('.fto-ref-view');
            const linkElem = viewDiv.parentElement.querySelector(`.fto-ref-link[data-view-id="${viewDiv.dataset.viewId}"]`);

            const buttonListSpan = document.createElement('span');
            buttonListSpan.classList.add('fto-ref-view-button-list');

            // 图钉📌按钮
            const pinSpan = document.createElement('span');
            pinSpan.classList.add('fto-ref-view-pin', 'fto-ref-view-button');
            pinSpan.textContent = "📌";
            pinSpan.addEventListener('click', () => {
                if (viewDiv.dataset.status === 'floating') {
                    linkElem.dataset.status = 'open';
                    viewDiv.dataset.status = 'open';
                } else {
                    linkElem.dataset.status = 'closed';
                    viewDiv.dataset.status = clickPinToCloseView ? 'closed' : 'floating';
                }
            });
            buttonListSpan.append(pinSpan);

            if (!viewDiv.isLoading &&
                (!ViewHelper.hasFetchingRefSucceeded(elem) || showRefreshButtonEvenIfRefContentLoaded)) {
                // 刷新🔄按钮
                const refreshSpan = document.createElement('span');
                refreshSpan.classList.add('fto-ref-view-refresh', 'fto-ref-view-button');
                refreshSpan.textContent = "🔄";
                refreshSpan.addEventListener('click', () => {
                    this.startLoadingViewContent(viewDiv, linkElem.dataset.refId, true);
                });
                Utils.insertAfter(pinSpan, refreshSpan);
                buttonListSpan.append(refreshSpan);
            }

            elem.prepend(buttonListSpan);
        }

        /**
         * 
         * @param {HTMLElement} linkElem 
         * @returns 
         */
        setupRefLink(linkElem) {
            linkElem.classList.add('fto-ref-link');
            // closed: 无固定显示 view; open: 有固定显示 view
            linkElem.dataset.status = 'closed';

            const r = /^>>No.(\d+)$/.exec(linkElem.textContent);
            if (!r) { return; }
            const refId = Number(r[1]);
            linkElem.dataset.refId = String(refId);

            const viewId = Utils.generateViewID();
            linkElem.dataset.viewId = viewId;

            const viewDiv = document.createElement('div');
            viewDiv.classList.add('fto-ref-view');
            // closed: 不显示; floating: 悬浮显示; open: 完整固定显示; collapsed: 折叠固定显示
            viewDiv.dataset.status = 'closed';
            viewDiv.dataset.viewId = viewId;

            viewDiv.style.setProperty('--offset-left', `${Utils.getCoords(linkElem).left}px`);

            Utils.insertAfter(linkElem, viewDiv);

            if (autoOpenRefWhenCached) {
                (async () => {
                    const refCache = await this.model.getRefCache(refId);
                    if (refCache) {
                        linkElem.dataset.status = 'open';
                        viewDiv.dataset.status = 'collapsed';
                        viewDiv.append(refCache);
                        this.setupContent(refCache);
                    }
                })()
            }

            // 处理悬浮
            linkElem.addEventListener('mouseenter', () => {
                if (viewDiv.dataset.status !== 'closed') {
                    viewDiv.dataset.isHovering = '1';
                    return;
                }
                viewDiv.dataset.status = 'floating';
                viewDiv.dataset.isHovering = '1';
                this.startLoadingViewContent(viewDiv, refId);
            });
            viewDiv.addEventListener('mouseenter', () => {
                viewDiv.dataset.isHovering = '1';
            });
            for (const elem of [linkElem, viewDiv]) {
                elem.addEventListener('mouseleave', () => {
                    if (viewDiv.dataset.status != 'floating') {
                        return;
                    }
                    delete viewDiv.dataset.isHovering;
                    (async () => {
                        setTimeout(() => {
                            if (!viewDiv.dataset.isHovering) {
                                viewDiv.dataset.status = 'closed';
                            }
                        }, 200);
                    })();
                });
            }

            // 处理折叠
            linkElem.addEventListener('click', () => {
                if (linkElem.dataset.status === 'closed'
                    || ['collapsed', 'floating'].includes(viewDiv.dataset.status)) {
                    linkElem.dataset.status = 'open';
                    viewDiv.dataset.status = 'open';
                } else if (viewDiv.clientHeight > collapsedHeight) {
                    viewDiv.dataset.status = 'collapsed';
                }
            });
            viewDiv.addEventListener('click', () => {
                if (viewDiv.dataset.status === 'collapsed') {
                    viewDiv.dataset.status = 'open';
                }
            });
        }

        /**
         * 
         * @param {HTMLElement} viewDiv 
         * @param {number} refId 
         * @param {boolean} force
         */
        startLoadingViewContent(viewDiv, refId, forced = false) {
            if (!forced && viewDiv.hasChildNodes()) {
                return;
            } else if (viewDiv.dataset.isLoading) { // TODO: 也可以强制从头重新加载?
                return;
            }
            viewDiv.dataset.isLoading = '1';

            const loadingSpan = document.createElement('span');
            loadingSpan.classList.add('fto-ref-view-loading');
            const loadingTextSpan = document.createElement('span');
            loadingTextSpan.dataset.waitedMilliseconds = '0';
            loadingTextSpan.textContent = "加载中… 0.00s";
            loadingSpan.append(loadingTextSpan);
            viewDiv.innerHTML = '';
            viewDiv.append(loadingSpan);
            const intervalId = setInterval(() => {
                if (viewDiv.dataset.isLoading) {
                    const milliseconds = Number(loadingTextSpan.dataset.waitedMilliseconds) + 20;
                    loadingTextSpan.textContent = `加载中… ${(milliseconds / 1000.0).toFixed(2)}s`;
                    loadingTextSpan.dataset.waitedMilliseconds = String(milliseconds);
                } else {
                    clearInterval(intervalId);
                }
            }, 20);
            this.setupButtons(loadingSpan);

            (async () => {
                const itemElement = await this.model.loadItemElement(refId, forced);
                delete viewDiv.dataset.isLoading;
                viewDiv.innerHTML = '';
                viewDiv.append(itemElement);
                this.setupContent(itemElement);
            })();
        }

        // /**
        //  * 
        //  * @param {String} viewId 
        //  * @param {HTMLElement} itemElement 
        //  */
        // updateViewContent(viewId, itemElement) {

        // }

    }

    class ViewHelper {
        /**
         * 
         * @param {HTMLElement} elem 
         */
        static getPosterID(elem) {
            if (!elem.classList.contains('.h-threads-info-uid')) {
                elem = elem.querySelector('.h-threads-info-uid');
            }
            const uid = elem.textContent;
            return /^ID:(.*)$/.exec(uid)[1];
        }

        /**
         * 
         * @param {HTMLElement} elem 
         */
        static getThreadID(elem) {
            if (!elem.classList.contains('.h-threads-info-id')) {
                elem = elem.querySelector('.h-threads-info-id');
            }
            const link = elem.getAttribute('href');
            const id = /^.*\/t\/(\d*).*$/.exec(link)[1];
            if (!id.length) {
                return null;
            }
            return Number(id);
        }

        /**
         * 
         * @param {HTMLElement} elem 
         */
        static getPostID(elem) {
            if (!elem.classList.contains('.h-threads-info-id')) {
                elem = elem.querySelector('.h-threads-info-id');
            }
            return Number(/^No.(\d+)$/.exec(elem.textContent)[1]);
        }

        /**
         * 
         * @param {HTMLElement} elem 
         * @returns 
         */
        static hasFetchingRefSucceeded(elem) {
            return !elem.parentElement.querySelector('.fto-ref-view-error');
        }
    }

    class Model {

        constructor() {
            this.refCache = {};
        }

        get isSupported() {
            if (!window.indexedDB || !window.fetch) {
                return false;
            }
            return true;
        }

        /**
         * 
         * @param {number} refId 
         * @returns {HTMLElement?}
         */
        async getRefCache(refId) {
            const elem = this.refCache[refId];
            if (!elem) { return null; }
            return elem.cloneNode(true);
        }
        /**
         * 
         * @param {number} refId 
         * @param {HTMLElement} rawItem
         * @param {'page' | 'global'} scope
         */
        async recordRef(refId, rawItem, scope = 'page') {
            this.refCache[refId] = rawItem.cloneNode(true);
        }

        /**
         * 
         * @param {number} refId 
         * @param {String} viewId
         * @param {boolean} ignoresCache
         */
        async loadItemElement(refId, ignoresCache = false) {
            const itemContainer = document.createElement('div');

            const itemCache = ignoresCache ? null : await this.getRefCache(refId);
            if (itemCache) {
                itemContainer.append(itemCache);
            } else {
                try {
                    const resp = await Utils.fetchWithTimeout(`/Home/Forum/ref?id=${refId}`, null, refFetchingTimeout);
                    itemContainer.innerHTML = await resp.text();
                } catch (e) {
                    let message;
                    if (e instanceof Error) {
                        if (e.message === 'Timeout') {
                            message = `获取引用内容超时!`;
                        } else {
                            message = `获取引用内容失败:${e.toString()}`;
                        }
                    } else {
                        message = `获取引用内容失败:${String(e)}`;
                    }
                    const errorSpan = document.createElement('span');
                    errorSpan.classList.add('fto-ref-view-error');
                    errorSpan.textContent = message;
                    return errorSpan;
                }
            }

            const item = itemContainer.firstChild;

            if (!ViewHelper.getThreadID(item)) {
                const errorSpan = document.createElement('span');
                errorSpan.classList.add('fto-ref-view-error');
                errorSpan.textContent = `引用内容不存在!`;
                this.recordRef(refId, item, 'page')
                return errorSpan;
            }

            this.recordRef(refId, item, 'global');
            return item;

        }
    }

    class Utils {

        // https://stackoverflow.com/a/59837035
        static generateViewID() {
            if (!Utils.currentGeneratedViewID) {
                Utils.currentGeneratedViewID = 0;
            }
            Utils.currentGeneratedViewID += 1;
            return String(Utils.currentGeneratedViewID);
        }

        /**
         * 
         * @param {Node} node 
         * @param {Node} newNode 
         */
        static insertAfter(node, newNode) {
            node.parentNode.insertBefore(newNode, node.nextSibling);
        }

        // https://stackoverflow.com/a/26230989
        static getCoords(elem) { // crossbrowser version
            var box = elem.getBoundingClientRect();

            var body = document.body;
            var docEl = document.documentElement;

            var scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
            var scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;

            var clientTop = docEl.clientTop || body.clientTop || 0;
            var clientLeft = docEl.clientLeft || body.clientLeft || 0;

            var top = box.top + scrollTop - clientTop;
            var left = box.left + scrollLeft - clientLeft;

            return { top: Math.round(top), left: Math.round(left) };
        }

        // https://stackoverflow.com/a/49857905
        // https://stackoverflow.com/a/50101022
        static fetchWithTimeout(url, options, timeout = 10000) {
            options = { ...(options || {}) };
            const controller = new AbortController();
            if (options.signal instanceof AbortSignal) {
                options.signal.addEventListener(function (ev) {
                    controller.signal.dispatchEvent.call(this, ev);
                });
            }
            options.signal = controller.signal;
            return Promise.race([
                fetch(url, options),
                new Promise((_, reject) => {
                    setTimeout(() => {
                        reject(new Error('Timeout'));
                        controller.abort();
                    }, timeout);
                })
            ]);
        }

    }

    entry();
})();