PieceOfGerman Progress

Backup buttons moved to left sidebar. Heatmap centered.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        PieceOfGerman Progress
// @namespace   Violentmonkey Scripts
// @match       https://www.pieceofgerman.com/*
// @grant       none
// @version     14.1
// @author      Gemini
// @description Backup buttons moved to left sidebar. Heatmap centered.
// ==/UserScript==

(function() {
    'use strict';

    const cleanPath = window.location.pathname;
    const STORAGE_KEY_DAYS = 'pog_finished_days_' + cleanPath;
    const STORAGE_KEY_LAST_URL = 'pog_last_visited_url';
    const STORAGE_KEY_CALENDAR = 'pog_study_calendar';

    let hasNavigated = false;

    // --- 1. CSS STYLES ---
    function injectStyles() {
        const style = document.createElement('style');
        style.innerHTML = `
            /* Toast Feedback */
            @keyframes pogSlideIn { 0% { transform: translate(-50%, -50px); opacity: 0; } 60% { transform: translate(-50%, 10px); opacity: 1; } 100% { transform: translate(-50%, 0); opacity: 1; } }
            @keyframes pogSlideOut { 0% { transform: translate(-50%, 0); opacity: 1; } 100% { transform: translate(-50%, -50px); opacity: 0; } }
            .pog-toast { position: fixed; top: 40px; left: 50%; transform: translateX(-50%); background: rgba(30, 30, 30, 0.85); backdrop-filter: blur(8px); color: white; padding: 12px 25px; border-radius: 50px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25); font-family: sans-serif; font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 10px; z-index: 10001; animation: pogSlideIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
            .pog-toast.success { border-bottom: 2px solid #4CAF50; }
            .pog-toast.undo { border-bottom: 2px solid #9E9E9E; }
            .pog-toast.party { border-bottom: 2px solid #9C27B0; }

            /* DASHBOARD CONTAINER */
            #pog-dashboard {
                position: relative; /* Necessary for absolute positioning of children */
                display: flex;
                flex-direction: column;
                align-items: center;
                justify-content: center;
                margin-top: 10px;
                padding-bottom: 15px;
                width: 100%;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            }

            /* STREAK & HEATMAP (CENTERED) */
            .pog-streak-counter {
                font-size: 18px; font-weight: bold; color: #555; margin-bottom: 8px;
                display: flex; align-items: center; gap: 8px;
            }
            .pog-streak-number { color: #FF9800; font-size: 22px; }

            .pog-heatmap-grid { display: flex; gap: 3px; margin-bottom: 12px; }
            .pog-day-sq { width: 12px; height: 12px; border-radius: 2px; background-color: #ebedf0; transition: transform 0.1s; }
            .pog-day-sq:hover { transform: scale(1.3); border: 1px solid #333; }

            .pog-lv-0 { background-color: #ebedf0; }
            .pog-lv-1 { background-color: #9be9a8; }
            .pog-lv-2 { background-color: #40c463; }
            .pog-lv-3 { background-color: #30a14e; }
            .pog-lv-4 { background-color: #216e39; }

            /* BACKUP BUTTONS (FAR LEFT & VERTICAL) */
            .pog-backup-controls {
                position: absolute;
                left: 0;
                top: 50%;
                transform: translateY(-50%); /* Center vertically */
                display: flex;
                flex-direction: column; /* Stack vertically */
                gap: 8px;
                padding-left: 10px;
            }
            .pog-btn-mini {
                padding: 6px 10px;
                font-size: 11px;
                border: 1px solid #ccc;
                background: white;
                color: #555;
                border-radius: 4px;
                cursor: pointer;
                transition: all 0.2s;
                text-align: left;
                box-shadow: 0 1px 3px rgba(0,0,0,0.1);
            }
            .pog-btn-mini:hover { background: #f0f0f0; border-color: #999; transform: translateX(2px); }
        `;
        document.head.appendChild(style);
    }
    injectStyles();

    // --- 2. BACKUP & RESTORE LOGIC ---
    function exportData() {
        const data = {};
        for (let i = 0; i < localStorage.length; i++) {
            const key = localStorage.key(i);
            if (key.startsWith('pog_')) {
                data[key] = localStorage.getItem(key);
            }
        }
        const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        const date = new Date().toISOString().split('T')[0];
        a.href = url;
        a.download = `pieceofgerman_backup_${date}.json`;
        a.click();
        URL.revokeObjectURL(url);
        showToast("Backup Saved!", "success");
    }

    function importData() {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json';
        input.onchange = e => {
            const file = e.target.files[0];
            if (!file) return;
            const reader = new FileReader();
            reader.onload = event => {
                try {
                    const data = JSON.parse(event.target.result);
                    let count = 0;
                    for (const key in data) {
                        if (key.startsWith('pog_')) {
                            localStorage.setItem(key, data[key]);
                            count++;
                        }
                    }
                    showToast(`Restored ${count} items! Reloading...`, "success");
                    setTimeout(() => window.location.reload(), 1500);
                } catch (err) {
                    alert("Error reading file: " + err);
                }
            };
            reader.readAsText(file);
        };
        input.click();
    }

    // --- 3. DASHBOARD UI ---
    function getTodayStr() { return new Date().toISOString().split('T')[0]; }
    function getCalendarData() { const d = localStorage.getItem(STORAGE_KEY_CALENDAR); return d ? JSON.parse(d) : {}; }

    function updateCalendar(amount) {
        const data = getCalendarData();
        const today = getTodayStr();
        if (!data[today]) data[today] = 0;
        data[today] += amount;
        if (data[today] < 0) data[today] = 0;
        localStorage.setItem(STORAGE_KEY_CALENDAR, JSON.stringify(data));
        renderDashboard();
    }

    function calculateStreak() {
        const data = getCalendarData();
        let streak = 0;
        let d = new Date();
        let dateStr = d.toISOString().split('T')[0];
        if (data[dateStr] > 0) streak++;
        while (true) {
            d.setDate(d.getDate() - 1);
            dateStr = d.toISOString().split('T')[0];
            if (data[dateStr] && data[dateStr] > 0) streak++;
            else break;
        }
        return streak;
    }

    function renderDashboard() {
        const headerContainer = document.querySelector('.theme-header > .zpcontainer');
        if (!headerContainer) return;

        let dash = document.getElementById('pog-dashboard');
        if (!dash) {
            dash = document.createElement('div');
            dash.id = 'pog-dashboard';
            headerContainer.appendChild(dash);
        } else {
            dash.innerHTML = '';
        }

        const streak = calculateStreak();
        const data = getCalendarData();

        // 1. Streak
        const streakDiv = document.createElement('div');
        streakDiv.className = 'pog-streak-counter';
        streakDiv.innerHTML = `<span>Current Streak:</span> <span class="pog-streak-number">🔥 ${streak}</span>`;
        dash.appendChild(streakDiv);

        // 2. Heatmap
        const grid = document.createElement('div');
        grid.className = 'pog-heatmap-grid';
        const daysToShow = 30;
        for (let i = daysToShow - 1; i >= 0; i--) {
            const d = new Date();
            d.setDate(d.getDate() - i);
            const dateStr = d.toISOString().split('T')[0];
            const count = data[dateStr] || 0;

            const sq = document.createElement('div');
            sq.className = 'pog-day-sq';
            sq.title = `${dateStr}: ${count} tasks`;

            if (count === 0) sq.classList.add('pog-lv-0');
            else if (count <= 2) sq.classList.add('pog-lv-1');
            else if (count <= 4) sq.classList.add('pog-lv-2');
            else if (count <= 6) sq.classList.add('pog-lv-3');
            else sq.classList.add('pog-lv-4');

            grid.appendChild(sq);
        }
        dash.appendChild(grid);

        // 3. Backup Controls (Moved to Left Sidebar)
        const controls = document.createElement('div');
        controls.className = 'pog-backup-controls';

        const btnExport = document.createElement('button');
        btnExport.className = 'pog-btn-mini';
        btnExport.innerText = '⬇ Backup';
        btnExport.onclick = exportData;

        const btnImport = document.createElement('button');
        btnImport.className = 'pog-btn-mini';
        btnImport.innerText = '⬆ Restore';
        btnImport.onclick = importData;

        controls.appendChild(btnExport);
        controls.appendChild(btnImport);
        dash.appendChild(controls);
    }

    // --- 4. STANDARD LOGIC ---
    function showToast(text, type='success') {
        const old = document.querySelector('.pog-toast');
        if (old) old.remove();
        const toast = document.createElement('div');
        toast.className = `pog-toast ${type}`;
        let icon = type === 'undo' ? '↩️' : (type === 'party' ? '🎉' : '✅');
        toast.innerHTML = `<span>${icon}</span> <span>${text}</span>`;
        document.body.appendChild(toast);
        setTimeout(() => {
            toast.style.animation = 'pogSlideOut 0.4s ease forwards';
            setTimeout(() => toast.remove(), 400);
        }, 2500);
    }

    function getFinishedDays() {
        const data = localStorage.getItem(STORAGE_KEY_DAYS);
        return data ? JSON.parse(data) : [];
    }

    function toggleDayFinished(dayName, isChecked) {
        let finished = getFinishedDays();
        if (isChecked) {
            if (!finished.includes(dayName)) {
                finished.push(dayName);
                updateCalendar(1);
            }
        } else {
            if (finished.includes(dayName)) {
                finished = finished.filter(d => d !== dayName);
                updateCalendar(-1);
            }
        }
        localStorage.setItem(STORAGE_KEY_DAYS, JSON.stringify(finished));
    }

    function addResumeButton(hasDaysOnPage) {
        if (window.location.href.includes('?continue')) return;
        if (document.getElementById('pog-resume-btn')) return;
        const btn = document.createElement('button');
        btn.id = 'pog-resume-btn';
        btn.innerText = '▶ Resume';
        Object.assign(btn.style, {
            position: 'fixed', bottom: '25px', right: '25px', zIndex: '10000',
            padding: '14px 24px', backgroundColor: '#2196F3', color: 'white',
            border: 'none', borderRadius: '50px', boxShadow: '0 4px 15px rgba(33, 150, 243, 0.4)',
            cursor: 'pointer', fontSize: '15px', fontWeight: 'bold', transition: 'transform 0.2s ease'
        });
        btn.onmouseover = () => btn.style.transform = 'scale(1.05)';
        btn.onmouseout = () => btn.style.transform = 'scale(1)';
        btn.addEventListener('click', () => {
            if (hasDaysOnPage) {
                const url = new URL(window.location.href);
                url.searchParams.set('continue', '1');
                window.location.href = url.toString();
            } else {
                const lastUrl = localStorage.getItem(STORAGE_KEY_LAST_URL);
                if (lastUrl) {
                    const target = new URL(lastUrl, window.location.origin);
                    target.searchParams.set('continue', '1');
                    window.location.href = target.toString();
                } else {
                    alert("No progress found yet! Please enter a course manually once.");
                }
            }
        });
        document.body.appendChild(btn);
    }

    function tryClickDay(targetDayString) {
        const allElements = document.querySelectorAll('div, span, li, button, a');
        for (let el of allElements) {
            if (el.innerText && el.innerText.trim() === targetDayString) {
                if (el.classList.contains('pog-tracker-container')) continue;
                const style = window.getComputedStyle(el);
                if (el.tagName === 'BUTTON' || el.tagName === 'A' || style.cursor === 'pointer') {
                    el.click(); return true;
                }
            }
        }
        return false;
    }

    function goToNextWeek() {
        const regex = /(\d+)$/;
        const match = window.location.pathname.match(regex);
        if (match) {
            const nextNum = parseInt(match[0], 10) + 1;
            const newPath = window.location.pathname.replace(regex, nextNum);
            showToast(`Week Complete! Jumping to Week ${nextNum}...`, 'party');
            setTimeout(() => {
                const newUrl = new URL(window.location.href);
                newUrl.pathname = newPath;
                newUrl.searchParams.set('continue', '1');
                window.location.href = newUrl.toString();
            }, 2000);
            return true;
        }
        return false;
    }

    // --- 5. MAIN LOOP ---
    function updateUI() {
        const finishedDays = getFinishedDays();
        renderDashboard();

        const allElements = document.querySelectorAll('*');
        let leafNodes = [];
        for (let el of allElements) {
             if (el.children.length === 0 && el.innerText && /^Day \d+$/.test(el.innerText.trim())) {
                 leafNodes.push(el);
             }
        }
        const hasDaysOnPage = leafNodes.length > 0;

        if (hasDaysOnPage) {
            if (localStorage.getItem(STORAGE_KEY_LAST_URL) !== window.location.pathname) {
                 localStorage.setItem(STORAGE_KEY_LAST_URL, window.location.pathname);
            }
        }

        addResumeButton(hasDaysOnPage);

        for (let leaf of leafNodes) {
            const dayName = leaf.innerText.trim();
            let container = leaf.closest('button') || leaf.closest('a') || leaf.closest('li') || leaf;
            if (container.getAttribute('data-pog-has-checkbox') === 'true') continue;

            const cb = document.createElement('input');
            cb.type = 'checkbox';
            cb.className = "pog-tracker-checkbox";
            cb.style.margin = '5px auto 0 auto'; cb.style.width = '16px'; cb.style.height = '16px';
            cb.style.accentColor = '#4CAF50'; cb.style.cursor = 'pointer';
            if (finishedDays.includes(dayName)) cb.checked = true;

            cb.onclick = (e) => {
                e.stopPropagation();
                toggleDayFinished(dayName, e.target.checked);
                if (e.target.checked) showToast(`${dayName} Completed`, 'success');
                else showToast(`${dayName} Unchecked`, 'undo');
            };

            if (window.getComputedStyle(container).display !== 'flex') {
                 container.style.display = 'flex'; container.style.flexDirection = 'column'; container.style.alignItems = 'center';
            } else { container.style.flexDirection = 'column'; }

            container.classList.add('pog-tracker-container');
            container.appendChild(cb);
            container.setAttribute('data-pog-has-checkbox', 'true');
            container.setAttribute('data-pog-dayname', dayName);
        }

        if (!hasNavigated && window.location.href.includes('?continue')) {
            const processedContainers = document.querySelectorAll('[data-pog-dayname]');
            if (processedContainers.length > 0) {
                 let targetDay = null;
                 let allDone = true;
                 for (let btn of processedContainers) {
                     const name = btn.getAttribute('data-pog-dayname');
                     if (!finishedDays.includes(name)) {
                         targetDay = name;
                         allDone = false;
                         break;
                     }
                 }
                 if (targetDay) {
                     if (tryClickDay(targetDay)) {
                         showToast(`Resuming: ${targetDay}`, 'success');
                         hasNavigated = true;
                     }
                 } else if (allDone) {
                     if (goToNextWeek()) hasNavigated = true;
                 }
            }
        }
    }

    setInterval(updateUI, 1000);

})();