Automatically check course names, provides a structured progress for discussions and assignments, also saves progress locally.
Verze ze dne
// ==UserScript==
// @name Tutorial Online Progress Widget
// @namespace http://tampermonkey.net/
// @version 1
// @description Automatically check course names, provides a structured progress for discussions and assignments, also saves progress locally.
// @author deoffuscated
// @match https://elearning.ut.ac.id/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Config
const APP_NAME = "Tutorial Online Progress Widget";
const WIDGET_ICON = 'https://suopmkm.ut.ac.id/uo/statics/logo.png';
const STORAGE_DATA_KEY = 'tuton_progress_checklist';
const STORAGE_COURSES_KEY = 'tuton_course_cache_list';
const STATE_KEY = 'tuton_widget_minimized_state';
const defaultCourses = [
"MATA KULIAH 1", "MATA KULIAH 2", "MATA KULIAH 3",
"MATA KULIAH 4", "MATA KULIAH 5", "MATA KULIAH 6",
"MATA KULIAH 7", "MATA KULIAH 8"// Untuk Fallback
];
const colLabels = [
"DISKUSI 1", "DISKUSI 2", "DISKUSI 3", "DISKUSI 4",
"DISKUSI 5", "DISKUSI 6", "DISKUSI 7", "DISKUSI 8",
"TUGAS 1", "TUGAS 2", "TUGAS 3"
];
// Animasi Confetti
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/confetti.browser.min.js';
document.head.appendChild(script);
function loadCachedCourses() {
const stored = localStorage.getItem(STORAGE_COURSES_KEY);
return stored ? JSON.parse(stored) : defaultCourses;
}
function saveCachedCourses(courses) {
localStorage.setItem(STORAGE_COURSES_KEY, JSON.stringify(courses));
}
function loadProgressData() { return JSON.parse(localStorage.getItem(STORAGE_DATA_KEY) || '{}'); }
function saveProgressData(data) { localStorage.setItem(STORAGE_DATA_KEY, JSON.stringify(data)); }
function loadWidgetState() { return localStorage.getItem(STATE_KEY) === 'true'; }
function saveWidgetState(isMin) { localStorage.setItem(STATE_KEY, isMin); }
// Check Daftar Mata Kuliah
let courseList = loadCachedCourses();
function scanForCourses() {
const courseElements = document.querySelectorAll('.coursename .multiline');
if (courseElements.length > 0) {
const scannedNames = Array.from(courseElements).map(el => {
let name = el.textContent.trim();
return name.replace(/\s+\d+$/, '');
});
if (JSON.stringify(scannedNames) !== JSON.stringify(courseList)) {
console.log(`[${APP_NAME}] Daftar mata kuliah diperbarui.`);
courseList = scannedNames;
saveCachedCourses(courseList);
const existingWrapper = document.getElementById('ut-helper-wrapper');
if (existingWrapper) existingWrapper.remove();
initUI();
}
}
}
const observer = new MutationObserver((mutations) => {
if (document.querySelector('.dashboard-card-deck')) {
scanForCourses();
}
});
observer.observe(document.body, { childList: true, subtree: true });
// CSS
const style = document.createElement('style');
style.innerHTML = `
:root {
--glass-bg: rgba(255, 255, 255, 0.85);
--glass-border: rgba(255, 255, 255, 0.4);
--glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.25);
--primary-color: #1859BC;
--accent-tugas: #e67e22;
--success-color: #2ecc71;
--text-dark: #1a202c;
--text-light: #4a5568;
--danger-color: #c0392b;
--row-hover: rgba(24, 89, 188, 0.1);
--border-color: rgba(0, 0, 0, 0.1);
--chk-border: #718096;
--chk-bg: rgba(255, 255, 255, 0.6);
}
#ut-helper-wrapper {
position: fixed; z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
#ut-helper-wrapper.minimized { bottom: 30px; right: 30px; top: auto; left: auto; transform: none; }
#ut-helper-wrapper.expanded { bottom: 30px; right: 30px; top: auto; left: auto; transform: none; }
/* TOMBOL WIDGET BULAT */
#ut-widget-trigger {
width: 55px; height: 55px; border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(4px);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
display: flex; align-items: center; justify-content: center;
cursor: pointer; border: 2px solid var(--primary-color);
animation: popIn 0.4s; padding: 8px; box-sizing: border-box;
}
#ut-widget-trigger:hover { transform: scale(1.05); box-shadow: 0 8px 25px rgba(24, 89, 188, 0.4); }
#ut-widget-trigger img { width: 100%; height: 100%; object-fit: contain; pointer-events: none; }
@keyframes popIn { from { transform: scale(0); } to { transform: scale(1); } }
/* PANEL UTAMA */
#ut-main-panel {
background: var(--glass-bg);
backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
border: 1px solid var(--glass-border);
box-shadow: var(--glass-shadow);
border-radius: 12px;
padding: 15px;
display: none; flex-direction: column;
/* Lebar dinamis agar fit dengan tab */
min-width: 400px;
max-width: 95vw;
animation: slideUp 0.3s ease-out;
transform-origin: bottom right;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* HEADER */
.ut-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.ut-title { font-weight: 700; color: var(--text-dark); font-size: 15px; display: flex; align-items: center; }
.ut-title img { margin-right: 10px; height: 24px; width: auto; object-fit: contain; pointer-events: none; }
.ut-close-icon {
cursor: pointer; color: var(--text-dark);
width: 28px; height: 28px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
transition: all 0.2s;
}
.ut-close-icon:hover { background: rgba(0,0,0,0.1); color: red; }
/* TABS (NAVIGATION) */
.ut-nav-tabs {
display: flex;
background: rgba(0,0,0,0.05);
padding: 4px;
border-radius: 8px;
margin-bottom: 10px;
gap: 5px;
}
.ut-tab-item {
flex: 1;
text-align: center;
padding: 6px 10px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
border-radius: 6px;
color: var(--text-light);
transition: all 0.2s;
user-select: none;
}
.ut-tab-item:hover { background: rgba(255,255,255,0.5); }
.ut-tab-item.active {
background: #fff;
color: var(--primary-color);
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
/* TABLE */
.ut-table { width: 100%; border-collapse: collapse; border-spacing: 0; margin-bottom: 5px; }
.ut-table tr { border-bottom: 1px solid var(--border-color); transition: background 0.15s ease; }
.ut-table tr:hover:not(:first-child) { background-color: var(--row-hover); }
.ut-table td { padding: 6px 4px; text-align: center; vertical-align: middle; }
/* LOGIC UNTUK MENYEMBUNYIKAN KOLOM (ACCORDION) */
.ut-table.mode-diskusi .type-tugas { display: none; }
.ut-table.mode-tugas .type-diskusi { display: none; }
.col-head {
font-size: 11px; font-weight: 800; color: var(--primary-color);
text-transform: uppercase; padding-bottom: 8px !important;
border-bottom: 2px solid var(--primary-color) !important;
}
.col-head-tugas {
font-size: 11px; font-weight: 800; color: var(--accent-tugas);
text-transform: uppercase; padding-bottom: 8px !important;
border-bottom: 2px solid var(--accent-tugas) !important;
}
.bg-tugas { background-color: rgba(230, 126, 34, 0.05); }
.row-label {
font-size: 11px; font-weight: 700; color: var(--text-dark);
text-align: left !important;
padding-left: 5px !important;
min-width: 150px; max-width: 200px;
text-transform: uppercase; line-height: 1.2;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.btn-reset {
cursor: pointer; width: 50px;
text-align: center !important; vertical-align: middle !important;
padding: 0 !important;
border-bottom: 2px solid var(--primary-color) !important;
font-size: 11px; font-weight: 900; color: var(--danger-color);
}
.btn-reset:hover { background-color: rgba(192, 57, 43, 0.1); }
/* CHECKBOX */
.ut-chk-wrap {
display: inline-block; position: relative; cursor: pointer;
width: 16px; height: 16px; user-select: none; top: 2px;
transition: opacity 0.3s;
}
.ut-chk-wrap.disabled { opacity: 0.3; pointer-events: none; filter: grayscale(1); }
.ut-chk-wrap input { opacity: 0; cursor: pointer; height: 0; width: 0; }
.checkmark {
position: absolute; top: 0; left: 0; height: 16px; width: 16px;
background-color: var(--chk-bg); border-radius: 4px;
border: 2px solid var(--chk-border); transition: all 0.15s ease-in-out;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.ut-chk-wrap:hover .checkmark { border-color: var(--primary-color); background-color: #fff; }
.ut-chk-wrap input:checked ~ .checkmark {
background-color: var(--primary-color); border-color: var(--primary-color);
box-shadow: 0 2px 4px rgba(24, 89, 188, 0.4);
}
.chk-tugas input:checked ~ .checkmark {
background-color: var(--accent-tugas); border-color: var(--accent-tugas);
box-shadow: 0 2px 4px rgba(230, 126, 34, 0.4);
}
.checkmark:after {
content: ""; position: absolute; display: none; left: 4px; top: 1px; width: 3px; height: 8px;
border: solid white; border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.ut-chk-wrap input:checked ~ .checkmark:after { display: block; }
/* PROGRESS BAR */
.prog-cont { margin-top: 10px; }
.prog-bg { background: rgba(0,0,0,0.1); border-radius: 20px; height: 6px; width: 100%; overflow: hidden; }
.prog-fill {
height: 100%;
background: var(--success-color);
width: 0%;
transition: width 0.5s;
border-radius: 20px;
}
.prog-text { font-size: 10px; text-align: right; margin-top: 4px; color: var(--text-dark); font-weight: 600; }
/* MODAL KONFIRMASI */
#ut-modal-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.4); backdrop-filter: blur(4px);
z-index: 10001; display: none; justify-content: center; align-items: center;
}
#ut-modal-box {
background: rgba(255,255,255,0.95); border: 1px solid rgba(255,255,255,0.5);
box-shadow: 0 20px 50px rgba(0,0,0,0.3); border-radius: 12px; padding: 25px; width: 300px;
text-align: center; transform: scale(0.95); animation: modalPop 0.2s forwards;
}
@keyframes modalPop { to { transform: scale(1); } }
.ut-modal-title { font-size: 16px; font-weight: 700; margin-bottom: 5px; color: var(--text-dark); }
.ut-modal-desc { font-size: 13px; color: var(--text-light); margin-bottom: 20px; }
.ut-modal-btns { display: flex; gap: 10px; }
.ut-btn { flex: 1; border: none; padding: 10px; border-radius: 6px; font-weight: 600; cursor: pointer; font-size: 12px; }
.btn-cancel { background: #edf2f7; color: var(--text-dark); }
.btn-cancel:hover { background: #e2e8f0; }
.btn-ok { background: var(--danger-color); color: white; }
.btn-ok:hover { background: #a93226; }
`;
document.head.appendChild(style);
// Load UI
function initUI() {
const wrapper = document.createElement('div');
wrapper.id = 'ut-helper-wrapper';
const widgetTrigger = document.createElement('div');
widgetTrigger.id = 'ut-widget-trigger';
widgetTrigger.innerHTML = `<img src="${WIDGET_ICON}" alt="UT Helper">`;
widgetTrigger.title = 'Open Progress Widget';
let tableHTML = `<table class="ut-table mode-all" id="ut-checklist-table">`;
tableHTML += `<tr><td class="btn-reset" id="ut-btn-reset" title="Reset Semua">RESET</td>`;
colLabels.forEach((l, index) => {
const isTugas = index >= 8;
const headerClass = isTugas ? 'col-head-tugas' : 'col-head';
const typeClass = isTugas ? 'type-tugas' : 'type-diskusi';
tableHTML += `<td class="${headerClass} ${typeClass}">${l}</td>`;
});
tableHTML += `</tr>`;
courseList.forEach(row => {
const cleanRow = row.replace(/[^a-zA-Z0-9]/g, '');
tableHTML += `<tr><td class="row-label" title="${row}">${row}</td>`;
for(let i=1; i<=11; i++) {
const isTugas = i >= 9;
const typeClass = isTugas ? 'type-tugas' : 'type-diskusi';
const bgClass = isTugas ? 'bg-tugas' : '';
const chkClass = isTugas ? 'ut-chk-wrap chk-tugas' : 'ut-chk-wrap';
tableHTML += `<td class="${bgClass} ${typeClass}"><label class="${chkClass}"><input type="checkbox" data-id="${cleanRow}_${i}"><span class="checkmark"></span></label></td>`;
}
tableHTML += `</tr>`;
});
tableHTML += `</table>`;
const mainPanel = document.createElement('div');
mainPanel.id = 'ut-main-panel';
mainPanel.innerHTML = `
<div class="ut-header">
<span class="ut-title"><img src="${WIDGET_ICON}" alt="icon"> Tutorial Online Progress Widget</span>
<div class="ut-close-icon" id="ut-btn-minimize" title="Tutup">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</div>
</div>
<!-- TAB NAVIGASI -->
<div class="ut-nav-tabs">
<div class="ut-tab-item active" data-mode="mode-all">Semua</div>
<div class="ut-tab-item" data-mode="mode-diskusi">Diskusi</div>
<div class="ut-tab-item" data-mode="mode-tugas">Tugas</div>
</div>
${tableHTML}
<div class="prog-cont">
<div class="prog-bg"><div class="prog-fill" id="ut-prog-bar"></div></div>
<div class="prog-text" id="ut-prog-lbl">0% Selesai</div>
</div>
`;
wrapper.appendChild(widgetTrigger);
wrapper.appendChild(mainPanel);
document.body.appendChild(wrapper);
const modalOverlay = document.createElement('div');
modalOverlay.id = 'ut-modal-overlay';
modalOverlay.innerHTML = `
<div id="ut-modal-box">
<div class="ut-modal-title">Reset Checklist?</div>
<div class="ut-modal-desc">Semua tanda progress akan dihapus.</div>
<div class="ut-modal-btns">
<button class="ut-btn btn-cancel" id="ut-modal-cancel">Batal</button>
<button class="ut-btn btn-ok" id="ut-modal-ok">Hapus</button>
</div>
</div>
`;
document.body.appendChild(modalOverlay);
// Logic Widget
const checkboxes = mainPanel.querySelectorAll('input[type="checkbox"]');
let progressData = loadProgressData();
let isMinimized = loadWidgetState();
const tabItems = mainPanel.querySelectorAll('.ut-tab-item');
const dataTable = document.getElementById('ut-checklist-table');
tabItems.forEach(tab => {
tab.addEventListener('click', () => {
tabItems.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
const mode = tab.getAttribute('data-mode');
dataTable.className = `ut-table ${mode}`;
});
});
function triggerCelebration() {
if (typeof confetti === 'function') {
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 },
zIndex: 10002
});
}
}
function calculateProgress(isUserAction = false) {
const total = checkboxes.length;
if (total === 0) return;
const checked = Array.from(checkboxes).filter(c => c.checked).length;
const pct = Math.round((checked / total) * 100);
document.getElementById('ut-prog-bar').style.width = `${pct}%`;
document.getElementById('ut-prog-lbl').innerText = `${pct}% Selesai`;
if (pct === 100 && isUserAction) {
triggerCelebration();
}
}
function applyRules(isUserAction = false) {
courseList.forEach(row => {
const cleanRow = row.replace(/[^a-zA-Z0-9]/g, '');
// Untuk Diskusi
for (let i = 1; i < 8; i++) {
const currentId = `${cleanRow}_${i}`;
const nextId = `${cleanRow}_${i+1}`;
toggleLinkedCheck(currentId, nextId);
}
// Untuk Tugas
toggleLinkedCheck(`${cleanRow}_3`, `${cleanRow}_9`); // Diskusi 3 -> Tugas 1
toggleLinkedCheck(`${cleanRow}_5`, `${cleanRow}_10`); // Diskusi 5 -> Tugas 2
toggleLinkedCheck(`${cleanRow}_7`, `${cleanRow}_11`); // Diskusi 7 -> Tugas 3
});
saveProgressData(progressData);
calculateProgress(isUserAction);
}
function toggleLinkedCheck(sourceId, targetId) {
const sourceChk = mainPanel.querySelector(`input[data-id="${sourceId}"]`);
const targetChk = mainPanel.querySelector(`input[data-id="${targetId}"]`);
if(sourceChk && targetChk) {
const targetContainer = targetChk.closest('.ut-chk-wrap');
if(sourceChk.checked) {
targetChk.disabled = false;
targetContainer.classList.remove('disabled');
} else {
targetChk.disabled = true;
targetContainer.classList.add('disabled');
if(targetChk.checked) {
targetChk.checked = false;
progressData[targetId] = false;
}
}
}
}
checkboxes.forEach(chk => {
const id = chk.getAttribute('data-id');
if (progressData[id]) chk.checked = true;
chk.addEventListener('change', (e) => {
progressData[id] = e.target.checked;
saveProgressData(progressData);
applyRules(true);
});
});
// Reset
const btnReset = document.getElementById('ut-btn-reset');
const btnCancel = document.getElementById('ut-modal-cancel');
const btnOk = document.getElementById('ut-modal-ok');
const closeModal = () => { modalOverlay.style.display = 'none'; };
btnReset.addEventListener('click', () => { modalOverlay.style.display = 'flex'; });
btnCancel.addEventListener('click', closeModal);
modalOverlay.addEventListener('click', (e) => { if(e.target === modalOverlay) closeModal(); });
btnOk.addEventListener('click', () => {
progressData = {};
saveProgressData(progressData);
checkboxes.forEach(chk => chk.checked = false);
applyRules(false);
closeModal();
});
// Minimize/Maximize
function updateWidgetState() {
if (isMinimized) {
wrapper.className = 'minimized'; mainPanel.style.display = 'none'; widgetTrigger.style.display = 'flex';
} else {
wrapper.className = 'expanded'; mainPanel.style.display = 'flex'; widgetTrigger.style.display = 'none';
}
saveWidgetState(isMinimized);
}
widgetTrigger.addEventListener('click', () => { isMinimized = false; updateWidgetState(); });
document.getElementById('ut-btn-minimize').addEventListener('click', () => { isMinimized = true; updateWidgetState(); });
updateWidgetState();
applyRules(false);
}
scanForCourses();
initUI();
})();