QOJ Better

Make QOJ great again!

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         QOJ Better
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Make QOJ great again!
// @match        https://qoj.ac/*
// @match        https://jiang.ly/*
// @match        https://huang.lt/*
// @match        https://contest.ucup.ac/*
// @match        https://oj.qiuly.org/*
// @grant        none
// @license      MIT
// @author       cyx
// ==/UserScript==

// 获取题号
function getProblemId() {
    const matchContest = location.pathname.match(/\/contest\/(\d+)\/problem\/(\d+)/);
    if (matchContest) return matchContest[2];
    const matchProblem = location.pathname.match(/\/problem\/(\d+)/);
    if (matchProblem) return matchProblem[1];
    return null;
}

// 获取用户名
function getUsername() {
    const userLink = document.querySelector('a.dropdown-item[href*="/user/profile/"]');
    if (userLink) {
        const match = userLink.href.match(/\/user\/profile\/([^/?#]+)/);
        if (match) return match[1];
    }
    return null;
}

function switchDomain() {
    if (document.getElementById('domain-switcher')) return;
    const currentHost = location.host;
    const pathname = location.pathname + location.search + location.hash;
    const isContest =
        pathname.includes('/contest/') ||
        pathname.includes('/contests') ||
        pathname.includes('/user') ||
        pathname.includes('/results');
    const domains = isContest
        ? ['qoj.ac', 'jiang.ly', 'huang.lt', 'oj.qiuly.org', 'contest.ucup.ac']
        : ['qoj.ac', 'jiang.ly', 'huang.lt', 'oj.qiuly.org'];

    // 构造域名切换内容
    const span = document.createElement('span');
    span.id = 'domain-switcher';
    span.style.fontSize = '0.9em';
    span.style.color = '#666';
    span.textContent = 'switch to: ';

    domains.forEach((domain, i) => {
        const link = document.createElement('a');
        link.textContent = domain;
        link.style.marginLeft = '4px';
        link.style.color = domain === currentHost ? '#999' : '#007bff';
        link.style.cursor = domain === currentHost ? 'default' : 'pointer';
        link.style.textDecoration = 'none';
        link.onmouseover = () => (link.style.textDecoration = 'underline');
        link.onmouseout = () => (link.style.textDecoration = 'none');
        if (domain !== currentHost) {
            link.onclick = () => (window.location.href = `https://${domain}${pathname}`);
        }
        span.appendChild(link);
        if (i < domains.length - 1) span.append(' ');
    });

    // 插入到合适位置
    const navbarUser = document.querySelector('.nav-link.dropdown-toggle');
    if (navbarUser) {
        const parentUl = navbarUser.closest('ul.navbar-nav, ul.nav');
        if (parentUl && !parentUl.querySelector('#domain-switcher')) {
            const li = document.createElement('li');
            li.className = 'nav-item d-flex align-items-center';
            li.appendChild(span);
            parentUl.insertBefore(li, navbarUser.closest('li.nav-item'));
        }
        return;
    }

    const navPills = document.querySelector('.nav.nav-pills.float-right');
    if (navPills && !navPills.querySelector('#domain-switcher')) {
        const li = document.createElement('li');
        li.className = 'nav-item d-flex align-items-center';
        li.appendChild(span);
        navPills.insertBefore(li, navPills.firstChild);
        return;
    }

    document.body.insertBefore(span, document.body.firstChild);
};
function backProblem() {
    if (document.querySelector('.nav-link.back-problem')) return;
    const nav = document.querySelector("ul.nav.nav-tabs");
    if (!nav) return;

    const match = location.pathname.match(/^\/contest\/(\d+)\/problem\/(\d+)/);
    if (!match) return;
    const pid = match[2];

    const li = document.createElement("li");
    li.className = "nav-item";
    li.innerHTML = `<a class="nav-link back-problem" href="/problem/${pid}" role="tab">Back to the problem</a>`;

    const backToContest = Array.from(nav.querySelectorAll("a")).find(a => a.textContent.includes("Back to the contest"));
    if (backToContest) backToContest.parentElement.before(li);
    else nav.appendChild(li);
};
function viewSubmissions() {
    if (document.querySelector('.nav-link.view-submissions')) return;
    const nav = document.querySelector('ul.nav.nav-tabs[role="tablist"]');
    if (!nav) return;

    const matchContest = location.pathname.match(/\/contest\/(\d+)\/problem\/(\d+)/);
    const matchProblem = location.pathname.match(/\/problem\/(\d+)/);
    const pid = matchContest ? matchContest[2] : matchProblem ? matchProblem[1] : null;
    if (!pid) return;

    const userLink = document.querySelector('a.dropdown-item[href*="/user/profile/"]');
    const username = userLink ? userLink.href.match(/profile\/([^/?#]+)/)?.[1] : null;
    if (!username) return;

    const li = document.createElement('li');
    li.className = 'nav-item';
    li.innerHTML = `<a class="nav-link view-submissions" href="/submissions?problem_id=${pid}&submitter=${username}" role="tab">View submissions</a>`;
    nav.appendChild(li);
};
function viewInContestLinks() {
    const alertBox = document.querySelector('.alert.alert-primary');
    if (!alertBox) return;

    const listItems = alertBox.querySelectorAll('ul.uoj-list li a[href*="/contest/"]');
    if (!listItems.length) return;

    const pidMatch = window.location.pathname.match(/\/problem\/(\d+)/);
    if (!pidMatch) return;
    const pid = pidMatch[1];

    listItems.forEach(a => {
        if (a.parentElement.querySelector('a[data-added="true"]')) return;
        const match = a.href.match(/\/contest\/(\d+)(\?v=\d+)?/);
        if (!match) return;
        const cid = match[1];
        const ver = match[2] || '';
        const viewLink = document.createElement('a');
        viewLink.textContent = '[view in contest]';
        viewLink.href = `/contest/${cid}/problem/${pid}${ver}`;
        viewLink.style.marginLeft = '4px';
        viewLink.dataset.added = 'true';
        a.insertAdjacentElement('afterend', viewLink);
    });
};
function addAcTag() {
    if (window.__qoj_fullscore_lock) return;
    window.__qoj_fullscore_lock = true;
    const pid = getProblemId();
    const username = getUsername();
    if (!pid || !username) return;
    try {
        const pid = getProblemId();
        const username = getUsername();
        if (!pid || !username) return;

        const infoRow = document.querySelector('.row.d-flex.justify-content-center');
        if (!infoRow) return;
        if (infoRow.querySelector('.badge-fullscore')) return;

        const totalEl = [...infoRow.querySelectorAll('.badge.badge-secondary')]
            .find(e => e.textContent.includes('Total points'));
        if (!totalEl) return;

        const total = parseFloat(totalEl.textContent.replace(/[^\d.]/g, ''));
        if (isNaN(total)) return;

        fetch(`/submissions?problem_id=${pid}&submitter=${username}&min_score=${total}&max_score=${total}`)
            .then(res => res.text())
            .then(html => {
                const match = html.match(/<td><a href="(\/submission\/\d+)">/);
                if (match) {
                    const sub = match[1];
                    const badge = document.createElement('a');
                    badge.className = 'badge badge-success mr-1 badge-fullscore';
                    badge.textContent = 'Accepted ✓';
                    badge.href = `${sub}`;
                    badge.target = '_blank';
                    infoRow.appendChild(badge);
                    const submitLink = document.querySelector('a.nav-link[href="#tab-submit-answer"]');
                    if (!submitLink) return;

                    if (submitLink.classList.contains('submit-green')) return;

                    submitLink.classList.add('submit-green');

                    const style = document.createElement('style');
                    style.textContent = `
                        a.nav-link.submit-green {
                            color: #00cc00 !important;
                        }
                        a.nav-link.submit-green:hover {
                            color: #00cc00 !important;
                        }
                    `;
                    document.head.appendChild(style);
                }
            })
            .catch(err => console.error('检测满分失败:', err))
            .finally(() => {
                setTimeout(() => { window.__qoj_fullscore_lock = false; }, 100);
            });
    } catch (e) {
        console.error(e);
        window.__qoj_fullscore_lock = false;
    }
}

(function () {
    'use strict';
    // --- 定义主函数 ---
    function main() {
        switchDomain();
        backProblem();
        viewSubmissions();
        viewInContestLinks();
        addAcTag();
    }

    // --- 初次执行 ---
    main();

    // --- 使用 MutationObserver 监听 DOM 动态变化 ---
    const observer = new MutationObserver(() => {
        // 检查关键元素是否存在
        const needRun =
            document.querySelector('.alert.alert-primary') || // 可能是 viewInContestLinks 所需
            document.querySelector('ul.nav.nav-tabs') || // viewSubmissions / backProblem
            document.querySelector('.nav-link.dropdown-toggle') || // 登录状态
            document.querySelector('.nav.nav-pills.float-right'); // 游客状态

        if (needRun) {
            observer.disconnect(); // 先断开,防止重复触发
            setTimeout(() => {
                main(); // 稍延迟再执行,确保元素已稳定渲染
                observer.observe(document.body, { childList: true, subtree: true }); // 重新监听
            }, 100);
        }
    });

    // 启动观察器
    observer.observe(document.body, { childList: true, subtree: true });
})();