Tiến cụt - HUSC

Panel filter ĐKLHP, tìm kiếm Inbox/CTĐT, hover modal xem nhanh (auto tiếng Việt, UX đẹp cho HUSC)

当前为 2025-09-10 提交的版本,查看 最新版本

// ==UserScript==
// @name         Tiến cụt - HUSC
// @namespace    http://tampermonkey.net/
// @version      2.6
// @description  Panel filter ĐKLHP, tìm kiếm Inbox/CTĐT, hover modal xem nhanh (auto tiếng Việt, UX đẹp cho HUSC)
// @author       TienCut
// @license      MIT
// @match        https://student.husc.edu.vn/Studying/Courses/*
// @match        https://student.husc.edu.vn/News*
// @match        https://student.husc.edu.vn/Message/Inbox*
// @match        https://student.husc.edu.vn/TrainingProgram*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // Xác định trang
    const isCourses = /\/Studying\/Courses\//.test(location.pathname);
    const isNews = /\/News/.test(location.pathname);
    const isInbox = /\/Message\/Inbox/.test(location.pathname);
    const isTraining = /\/TrainingProgram/.test(location.pathname);


    // Tạo panel Tiến cụt luôn xuất hiện, liệt kê chức năng theo từng trang
    let panel = document.createElement('div');
    panel.id = 'tiencut-panel';
let panelHTML = `
    <div style="padding:11px 16px 13px 16px; background:#fffbe9; border:1.7px solid #ecd9b6; position:fixed; top:18px; right:25px; z-index:10010; box-shadow:0 1px 8px #bbb5; border-radius:11px; min-width:170px; max-width:240px;font-size:14px;">
        <div style="font-weight:bold; font-size:15.7px; text-align:center;">
            Tiến cụt
        </div>
        <hr style="border:0; border-top:1.35px solid #f2c77d; margin:8px 0 10px 0;">
        <div id="tiencut-courses" style="display:${isCourses?'':'none'}">
            <div>- Ẩn lớp đã đầy bằng checkbox</div>
        </div>
        <div id="tiencut-training" style="display:${isTraining?'':'none'}">
            <div>- Tìm kiếm tên học phần, học kỳ</div>
            <input id="searchMon" type="text" placeholder="Tên học phần..." style="width:97%;padding:2px 4px;font-size:12px;margin:7px 0 5px 0;border:1px solid #b1d18b;border-radius:4px;outline:none;box-sizing:border-box">
            <input id="searchHK" type="text" placeholder="Học kỳ..." style="width:97%;padding:2px 4px;font-size:12px;margin-bottom:5px;border:1px solid #b1d18b;border-radius:4px;outline:none;box-sizing:border-box">
        </div>
        <div id="tiencut-inbox" style="display:${isInbox?'':'none'}">
            <div>- Di chuột vào tiêu đề để xem nhanh nội dung tin nhắn</div>
            <input id="searchInboxSender" type="text" placeholder="Tìm theo tên người gửi..." style="width:97%;padding:2px 4px;font-size:12px;margin:7px 0 5px 0;border:1px solid #b1d18b;border-radius:4px;outline:none;box-sizing:border-box">
        </div>
        <div id="tiencut-news" style="display:${isNews?'':'none'}">
            <div>- Di chuột vào link thông báo để xem nhanh nội dung</div>
        </div>
        <hr style="border:0; border-top:1.15px solid #efb35b; margin:10px 0 7px 0;">
        <div style="text-align:center; font-size:13px;color:#666;">
            Góp ý qua
            <a href="https://www.facebook.com/tiencut2711" target="_blank" style="display:inline-block; vertical-align:middle; margin-left:7px;">
                <img src="https://upload.wikimedia.org/wikipedia/commons/6/6c/Facebook_Logo_2023.png" style="height:19px;width:19px;border-radius:3px;vertical-align:middle;margin-bottom:2px;" alt="fb"/>
            </a>
        </div>
    </div>
`;


    panel.innerHTML = panelHTML;
    document.body.appendChild(panel);

    if (isInbox) {
    function filterInboxRows() {
        let table = document.querySelector('table');
        if(!table) return;
        let ths = Array.from(table.querySelectorAll('thead th, tr:first-child th, tr:first-child td'));
        // Tìm chỉ số cột tên người gửi, thường là cột thứ 2 -> thử tìm "người gửi" hoặc "tên" hoặc cột thứ 1 nếu không có
        let senderIdx = ths.findIndex(th => /gửi|sender|tên/i.test(th.textContent));
        if (senderIdx === -1) senderIdx = 1;  // fallback nếu không tìm ra thì là cột 1
        let key = document.getElementById('searchInboxSender').value.trim().toLowerCase();
        table.querySelectorAll('tbody tr').forEach(tr => {
            let tds = tr.querySelectorAll('td');
            if(!tds[senderIdx]) {
                tr.style.display='';
                return;
            }
            let content = tds[senderIdx].textContent.toLowerCase();
            tr.style.display = (!key || content.includes(key)) ? '' : 'none';
        });
    }
    document.getElementById('searchInboxSender').addEventListener('input', filterInboxRows);
    window.addEventListener('DOMContentLoaded', filterInboxRows);
}


    // Nếu là Training Program: filter tìm kiếm
if (isTraining) {
    function filterRowsCTDT() {
        let table = document.querySelector('table');
        if(!table) return;
        let ths = Array.from(table.querySelectorAll('thead th, tr:first-child th, tr:first-child td'));
        let monIndex = ths.findIndex(th => th.textContent.replace(/\s+/g, ' ').toLowerCase().includes('tên học phần') || th.textContent.toLowerCase().includes('môn học'));
        let hkIndex = ths.findIndex(th => /học[\s_-]*kỳ|hk|dự kiến/i.test(th.textContent));
        if(monIndex < 0 || hkIndex < 0) return;
        let nameValue = document.getElementById('searchMon').value.trim().toLowerCase();
        let hkValue = document.getElementById('searchHK').value.trim().toLowerCase();
        table.querySelectorAll('tbody tr').forEach(tr => {
            let tds = tr.querySelectorAll('td');
            if(tds.length<=Math.max(monIndex,hkIndex)) {
                tr.style.display='';
                return;
            }
            let match = true;
            if(nameValue && !tds[monIndex].textContent.toLowerCase().includes(nameValue)) match = false;
            if(hkValue && !tds[hkIndex].textContent.toLowerCase().includes(hkValue)) match = false;
            tr.style.display = match ? '' : 'none';
        });
    }
    document.getElementById('searchMon').addEventListener('input', filterRowsCTDT);
    document.getElementById('searchHK').addEventListener('input', filterRowsCTDT);
    window.addEventListener('DOMContentLoaded', filterRowsCTDT);
}


    // PHẦN CHỨC NĂNG TÙY TRANG
    // -- Nếu Courses: thêm checkbox filter lớp đã đầy
    if (isCourses) {
        let label = document.createElement('label');
        label.style = "display:block; margin:10px 0 5px 0;font-size:13.5px;";
        label.innerHTML = `<input type="checkbox" id="hideFullRows" style="vertical-align:middle; margin-right:4px"> Ẩn lớp đã đầy`;
        panel.querySelector('#tiencut-courses').appendChild(label);

        function parseInfo(cell) {
            let html = cell.innerHTML;
            let matches = html.match(/<b>(\d+)<\/b>.*?\/.*?(\d+)\s*$/i);
            if(matches && matches.length === 3){
                return {dk: parseInt(matches[1],10), td: parseInt(matches[2],10)};
            } else {
                let nums = html.replace(/<[^>]*>/g,'').split('/').map(x=>parseInt(x));
                if(nums.length >= 2){
                    return {dk: nums[0], td: nums[nums.length-1]};
                }
            }
            return null;
        }
        function filterRowsByFullbox() {
            let table = document.querySelector('table.table-striped');
            if (!table) return;
            let ths = Array.from(table.querySelectorAll('thead th'));
            let svIndex = ths.findIndex(th => th.textContent.replace(/\s+/g, ' ').toLowerCase().includes('số sv'));
            table.querySelectorAll('tbody tr').forEach(tr => {
                let tds = tr.querySelectorAll('td');
                if(tds.length<=svIndex) {
                    tr.style.display = '';
                    return;
                }
                let info = parseInfo(tds[svIndex]);
                if(document.getElementById('hideFullRows').checked && info && info.dk >= info.td)
                    tr.style.display = 'none';
                else
                    tr.style.display = '';
            });
        }
        document.getElementById('hideFullRows').addEventListener('change', filterRowsByFullbox);
        window.addEventListener('DOMContentLoaded', filterRowsByFullbox);
    }

    // ===== CSS Modal dùng chung =====
    GM_addStyle(`
      #husc-modal {
        position:fixed;top:60px;left:50%;transform:translateX(-50%);
        background:#fff;box-shadow:0 2px 22px #2227;padding:18px 22px 14px 22px;
        z-index:99999;max-width:520px;min-width:180px;max-height:75vh;
        overflow:auto;border-radius:10px;display:none;font-size:15.2px;
        line-height:1.58; transition: opacity 0.15s;
        animation:fade-in 0.13s;
      }
      #husc-modal-close {
        position:absolute;right:17px;top:7px;cursor:pointer;font-weight:bold;color:#e00;font-size:22px;z-index:100000;
      }
      @keyframes fade-in { from {opacity:0;} to {opacity:1;} }
    `);

    // ======= Modal dùng chung cho cả 2 chức năng =======
    let modal = document.createElement('div');
    modal.id = 'husc-modal';
    modal.innerHTML = '<span id="husc-modal-close">&times;</span><div id="husc-modal-content"></div>';
    document.body.appendChild(modal);
    const modalContent = document.getElementById('husc-modal-content');
    let modalHideTimeout = null;
    document.getElementById('husc-modal-close').onclick = function() { modal.style.display='none'; };
    modal.addEventListener('mouseenter', () => { clearTimeout(modalHideTimeout); modal.style.display='block'; });
    modal.addEventListener('mouseleave', () => {
        modalHideTimeout = setTimeout(()=>{ modal.style.display='none'; }, 250);
    });

    // ===== Hover News =====
    if(location.pathname.startsWith('/News')) {
        document.addEventListener('mouseover', function(e){
            let link = e.target.closest('a[href*="/News/Content/"]');
            if(link && !link.dataset.huscPreviewed){
                link.dataset.huscPreviewed = 1;
                link.addEventListener('mouseenter', function(){
                    clearTimeout(modalHideTimeout);
                    modal.style.display='block';
                    modalContent.innerHTML = 'Đang tải nội dung...';
                    GM_xmlhttpRequest({
                        method: "GET",
                        url: link.href,
                        onload: function(resp){
                            let parser = new DOMParser();
                            let doc = parser.parseFromString(resp.responseText, 'text/html');
                            let result = null;
                            let h2 = Array.from(doc.querySelectorAll('h2')).find(el => el.textContent.trim().toUpperCase().includes('THÔNG BÁO'));
                            if(h2) {
                                let next = h2.nextElementSibling;
                                while (next && !(next.classList && next.classList.contains('container-fluid'))) next = next.nextElementSibling;
                                if(next && next.classList.contains('container-fluid')) result = next;
                            }
                            if (!result) result = doc.querySelector('.panel-main-content .hitec-content .row > .col-xs-12');
                            modalContent.innerHTML = result ? result.innerHTML : 'Không tìm thấy nội dung thông báo!';
                        },
                        onerror: function(){ modalContent.innerHTML = 'Lỗi tải trang!'; }
                    });
                });
                link.addEventListener('mouseleave', function(){
                    modalHideTimeout = setTimeout(()=>{ if(!modal.matches(':hover')) modal.style.display='none'; }, 200);
                });
            }
        });
    }

    // ===== Hover Inbox =====
    if(location.pathname.startsWith('/Message/Inbox')) {
        function extractMessageBody(doc) {
            let bodyBlock = doc.querySelector('.panel-main-content .container-fluid:last-of-type')
            || doc.querySelector('.hitec-content .container-fluid:last-of-type')
            || doc.querySelector('.panel-main-content')
            || doc.querySelector('.hitec-content');
            if (!bodyBlock) {
                let divs = doc.querySelectorAll('.container-fluid');
                for (let i=divs.length-1; i>=0; i--) {
                    if(divs[i].querySelector('p')) {
                        bodyBlock = divs[i];
                        break;
                    }
                }
            }
            return bodyBlock ? bodyBlock.innerHTML : '<i>Không đọc được nội dung</i>';
        }
        document.addEventListener('mouseover', function(e){
            let link = e.target.closest('a[href^="/Message/Details/"], a[href^="/Message/View/"]');
            if(link && !link.dataset.huscPreviewed){
                link.dataset.huscPreviewed = 1;
                link.addEventListener('mouseenter', function(){
                    let msgUrl = link.getAttribute('href');
                    if(!msgUrl) return;
                    clearTimeout(modalHideTimeout);
                    modal.style.display = 'block';
                    modalContent.innerHTML = '<i>Đang tải nội dung...</i>';
                    GM_xmlhttpRequest({
                        method: "GET",
                        url: link.href,
                        onload: function(resp){
                            let parser = new DOMParser();
                            let doc = parser.parseFromString(resp.responseText, 'text/html');
                            let content = extractMessageBody(doc);
                            content = content.replace(/<footer[\s\S]*?footer>/gi,'').replace(/<script[\s\S]*?script>/gi,'');
                            modalContent.innerHTML = content.slice(0,1800);
                        },
                        onerror: function(){ modalContent.innerHTML = 'Lỗi tải trang!'; }
                    });
                });
                link.addEventListener('mouseleave', function(){
                    modalHideTimeout = setTimeout(()=>{ if(!modal.matches(':hover')) modal.style.display='none'; }, 180);
                });
            }
        });
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址