GitHub Issues 默认过滤器

替换 GitHub issues 的默认过滤器

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name               GitHub Default Issues Filter
// @name:zh-CN         GitHub Issues 默认过滤器
// @description        Replaces GitHub issue's default filter
// @description:zh-CN  替换 GitHub issues 的默认过滤器
// @version            0.1
// @author             guansss
// @namespace          https://github.com/guansss
// @source             https://github.com/guansss/userscripts
// @supportURL         https://github.com/guansss/userscripts/issues
// @match              *://github.com/*
// @run-at             document-start
// @grant              GM_info
// @license            MIT
// @noframes
// ==/UserScript==

function _script_main() {
    'use strict';

    const ready = new Promise((resolve) => {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => resolve);
        } else {
            resolve();
        }
    });

    function hookMethod(object, method, callback) {
        const original = object[method];

        object[method] = function () {
            callback.apply(this, arguments);
            return original.apply(this, arguments);
        };
    }

    let log;

    setLogger(console.log);

    function setLogger(logger) {
        log = logger.bind(console, `[${GM_info.script.name}]`);
    }

    /**
     * Periodically calls given function until it returns true.
     */
    function repeat(fn, interval = 200) {
        if (fn()) {
            return 0;
        }

        const id = setInterval(() => {
            try {
                fn() && clearInterval(id);
            } catch (e) {
                log(e);
                clearInterval(id);
            }
        }, interval);

        return id;
    }

    /**
     * Periodically calls given function until the return value is truthy.
     * @returns A CancelablePromise that resolves with the function's return value when truthy.
     */
    function until(fn, interval = 0) {
        let cancelled = false;

        const promise = new Promise((resolve, reject) =>
            repeat(() => {
                if (cancelled) {
                    return true;
                }

                try {
                    const result = fn();

                    if (result) {
                        resolve(result);

                        // break the repeat() loop
                        return true;
                    }
                } catch (e) {
                    reject(e);
                    return true;
                }
            }, interval)
        );

        promise.cancel = () => (cancelled = true);

        return promise;
    }

    const urlRegex = /.*issues\/?$/;
    const defaultFilters = 'is:issue is:open ';
    const targetFilters = 'is:issue';

    let currentTask;

    function cancelCurrentTask() {
        currentTask && currentTask.cancel();
    }

    function check(url) {
        if (urlRegex.test(url)) {
            cancelCurrentTask();

            currentTask = until(() => {
                const input = document.getElementById('js-issues-search');

                if (input && input.parentElement && input.parentElement.tagName === 'FORM') {
                    // when navigating from pull requests to issues, the input is persisted until navigation completes,
                    // so we should ensure the input value is as expected
                    if (input.value === defaultFilters) {
                        input.value = targetFilters;
                        input.parentElement.dispatchEvent(new SubmitEvent('submit', { bubbles: true }));

                        return true;
                    }

                    // cancel current task whenever the input is changed by user, usually this won't happen
                    // since the interval is very short, but just in case the user is Shining Finger
                    input.addEventListener('input', cancelCurrentTask, { once: true });
                }
            }, 100);
        }
    }

    const handleStateChange = (data, unused, url) => {
        if (url instanceof URL) {
            url = url.href;
        }

        if (url) {
            check(url);
        }
    };

    hookMethod(history, 'pushState', handleStateChange);
    hookMethod(history, 'replaceState', handleStateChange);

    ready.then(() => check(location.href));
}

_script_main();