// ==UserScript==
// @name Tiến cụt - HUSC
// @namespace http://tampermonkey.net/
// @version 2.9
// @description Tìm kiếm (sinh viên trong danh sách các lớp học phần, học phần trong CTĐT). Rê chuột để xem nhanh thông báo, tin nhắn....
// @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; bottom: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 style="font-size:13px">Tìm nhóm sinh viên:</div>
<input id="findStudentAllClassGroup" type="text" placeholder="Nhập mã SV hoặc tên..." style="width:98%;padding:3px 5px;font-size:12.8px;margin:5px 0 0 0;border:1px solid #b1d18b;border-radius:4px;">
<div id="findStudentAllClassGroupResult" style="margin:4px 0 0 0;font-size:13.5px;min-height:22px;"></div>
<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);
// --- Tìm sinh viên nhóm bất kỳ ---
const inputAllGroup = document.getElementById('findStudentAllClassGroup');
const resultDivAllGroup = document.getElementById('findStudentAllClassGroupResult');
let currentRequestId = 0;
if (inputAllGroup) inputAllGroup.addEventListener('input', function() {
const keyword = inputAllGroup.value.trim().toLowerCase();
resultDivAllGroup.innerHTML = '';
if (keyword.length < 2) {
if (keyword.length === 0) resultDivAllGroup.innerHTML = '';
else resultDivAllGroup.innerHTML = '<i>Nhập tối thiểu 2 ký tự...</i>';
return;
}
resultDivAllGroup.innerHTML = '<i>Đang kiểm tra các nhóm...</i>';
const links = Array.from(document.querySelectorAll('a[href^="/Course/Details/"]'));
if (!links.length) {
resultDivAllGroup.innerHTML = '<span style="color:#d00">Không tìm thấy danh sách nhóm học phần để tra cứu!</span>';
return;
}
const thisRequestId = ++currentRequestId;
let pending = links.length;
let found = [];
links.forEach(link => {
GM_xmlhttpRequest({
method: "GET",
url: link.href,
onload: function(resp) {
if (thisRequestId !== currentRequestId) return;
let parser = new DOMParser();
let doc = parser.parseFromString(resp.responseText, 'text/html');
// ƯU TIÊN lấy groupName là tên lớp/phần nhóm rõ ràng - thường nằm ở phần Thông tin lớp học phần
let groupName = '';
let pTitle = doc.querySelector('.form-control-static');
if (pTitle && /nhóm/i.test(pTitle.textContent)) {
groupName = pTitle.textContent.trim();
} else {
// Tìm ở small hoặc legend nếu có
let small = doc.querySelector('small');
if(small && /nhóm/i.test(small.textContent)) {
groupName = small.textContent.trim();
} else {
// fallback
groupName = (doc.querySelector('h2')?.textContent || link.textContent || 'Lớp/Nhóm').replace(/thông tin về lớp học phần/i, '').trim();
}
}
let rows = doc.querySelectorAll('#courseStudents table tbody tr');
rows.forEach(tr => {
let tds = tr.querySelectorAll('td');
if (!tds.length) return;
let msv = tds[1]?.textContent.trim() || '';
let hoten = ((tds[2]?.textContent || '') + ' ' + (tds[3]?.textContent || '')).trim();
if (
msv.toLowerCase().includes(keyword) ||
hoten.toLowerCase().includes(keyword)
) {
found.push({
group: groupName,
msv, hoten,
href: link.href
});
}
});
pending--;
if (pending === 0 && thisRequestId === currentRequestId) {
if (found.length === 0) {
resultDivAllGroup.innerHTML = '<span style="color:#d00">Không tìm thấy sinh viên nào phù hợp.</span>';
} else {
resultDivAllGroup.innerHTML = found.map(e =>
`<div>
<b><a href="${e.href}" target="_blank" style="color:#1976D2;text-decoration:underline">${e.group}</a></b>:
<span style="color:#116900">${e.hoten}</span>
(<span style="color:#991">${e.msv}</span>)
</div>`
).join('');
}
}
},
onerror: function() {
pending--;
if (pending === 0 && thisRequestId === currentRequestId && found.length === 0) {
resultDivAllGroup.innerHTML = '<span style="color:#d00">Lỗi tải dữ liệu nhóm.</span>';
}
}
});
});
});
}
// ===== 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">×</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);
});
}
});
}
})();
(function() {
// Lắng nghe hover/click vào link đổi học kỳ
let configLink = document.querySelector('a[title*="thay đổi ngành học"]');
if (!configLink) return;
let popupBox = null;
let popupOverlay = null;
function closePopup() {
popupBox && popupBox.remove();
popupBox = null;
popupOverlay && popupOverlay.remove();
popupOverlay = null;
}
configLink.addEventListener('click', async function(e) {
e.preventDefault();
if (popupBox) { closePopup(); return; }
// Tạo overlay mờ ngoài (nếu muốn)
popupOverlay = document.createElement('div');
popupOverlay.style.position = 'fixed';
popupOverlay.style.left = 0; popupOverlay.style.top = 0;
popupOverlay.style.width = '100vw';
popupOverlay.style.height = '100vh';
popupOverlay.style.zIndex = 10011;
popupOverlay.onclick = closePopup;
document.body.appendChild(popupOverlay);
// Tạo popup
popupBox = document.createElement('div');
popupBox.id = 'popup-chon-hocky';
popupBox.style = `
position:fixed;
top:${configLink.getBoundingClientRect().bottom + window.scrollY + 7}px;
left:${configLink.getBoundingClientRect().left + window.scrollX}px;
border:1.8px solid #f2c365;
background:#fffdfa;
max-width:480px;
min-width:310px;
max-height:380px;
overflow:auto;
border-radius:10px;
box-shadow:0px 3px 24px #bbb7;
padding:16px 10px 13px 18px;
z-index:10012;
`;
popupBox.innerHTML = '<div style="text-align:center;font-size:15px;font-weight:600;color:#e08a00">ĐANG TẢI...</div>';
document.body.appendChild(popupBox);
// Lấy HTML cấu hình học kỳ
GM_xmlhttpRequest({
method: "GET",
url: "/Setting/Change",
onload: function(resp) {
let parser = new DOMParser();
let doc = parser.parseFromString(resp.responseText, 'text/html');
let content = doc.querySelector('form[action="/Setting/Change"]');
if (!content) { popupBox.innerHTML = "Không lấy được dữ liệu."; return; }
// Copy block (bạn có thể làm đẹp thêm nếu muốn)
popupBox.innerHTML = content.outerHTML;
let form = popupBox.querySelector('form') || popupBox.querySelector('form[action="/Setting/Change"]');
if (!form) form = popupBox.querySelector('form');
// Nếu không có thẻ <form>, tìm lại vùng chứa radio
// Đảm bảo nút "Tác nghiệp" hoạt động đúng
let submitButton = popupBox.querySelector('button[type=submit], .btn.btn-primary');
if (form && submitButton) {
submitButton.addEventListener('click', function(ev){
ev.preventDefault();
let value = popupBox.querySelector('input[name="v"]:checked');
if (!value) {
alert('Bạn phải chọn một học kỳ trước!');
return;
}
// Gửi request đúng value
let data = "v=" + encodeURIComponent(value.value);
GM_xmlhttpRequest({
method:"POST",
url:"/Setting/Change",
data: data,
headers:{'Content-Type':'application/x-www-form-urlencoded'},
onload:function(r){
// Thành công → reload trang
closePopup();
location.reload();
},
onerror:function(){alert('Lỗi đổi học kỳ!');}
});
});
}
// Auto-close khi click ngoài popup
setTimeout(()=>{ // để tránh vừa click mở đã auto close!
window.addEventListener('mousedown', onOutsideClick);
},100);
function onOutsideClick(ev){
if (popupBox && !popupBox.contains(ev.target)) {
closePopup();
window.removeEventListener('mousedown', onOutsideClick);
}
}
}
});
});
})();