WebUntis Topic Scraper

Scrapt Lehrstoff vollautomatisch von den einzelnen Unterrichtsstunden

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         WebUntis Topic Scraper
// @namespace    http://tampermonkey.net/
// @namespace    https://github.com/dein-username
// @license      MIT
// @homepageURL  https://greasyfork.org/
// @version      0.1.0
// @description  Scrapt Lehrstoff vollautomatisch von den einzelnen Unterrichtsstunden
// @author       Whiztech
// @match        https://gds2.webuntis.com/timetable/my-student*
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function () {
    'use strict';

    function icon(paths, size) {
        size = size || 15;
        var ns = 'http://www.w3.org/2000/svg';
        var svg = document.createElementNS(ns, 'svg');
        svg.setAttribute('width',  size);
        svg.setAttribute('height', size);
        svg.setAttribute('viewBox', '0 0 24 24');
        svg.setAttribute('fill', 'none');
        svg.setAttribute('stroke', 'currentColor');
        svg.setAttribute('stroke-width', '2');
        svg.setAttribute('stroke-linecap', 'round');
        svg.setAttribute('stroke-linejoin', 'round');
        svg.style.cssText = 'display:inline-block;vertical-align:middle;flex-shrink:0;';
        paths.forEach(function(d) {
            var p = document.createElementNS(ns, 'path');
            p.setAttribute('d', d);
            svg.appendChild(p);
        });
        return svg;
    }

    var ICONS = {
        play:      ['M5 3l14 9-14 9V3z'],
        copy:      ['M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-2','M16 2H8a2 2 0 0 0-2 2v0a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v0a2 2 0 0 0-2-2z'],
        eye:       ['M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z','M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z'],
        trash:     ['M3 6h18','M19 6l-1 14H6L5 6','M8 6V4h8v2','M10 11v6','M14 11v6'],
        check:     ['M20 6L9 17l-5-5'],
        xmark:     ['M18 6 6 18','M6 6l12 12'],
        clipboard: ['M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2','M9 2h6a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z'],
        loader:    ['M12 2v4','M12 18v4','M4.93 4.93l2.83 2.83','M16.24 16.24l2.83 2.83','M2 12h4','M18 12h4','M4.93 19.07l2.83-2.83','M16.24 7.76l2.83-2.83'],
    };

    function makeBtn(labelText, iconName, bg, bgHover) {
        var btn = document.createElement('button');
        btn.style.cssText = [
            'width:100%;padding:8px 10px;background:' + bg + ';color:#fff;',
            'border:none;border-radius:8px;font-size:12.5px;font-weight:600;cursor:pointer;',
            'transition:background .15s;margin-bottom:6px;',
            'display:flex;align-items:center;gap:7px;',
        ].join('');
        btn.appendChild(icon(ICONS[iconName], 14));
        var span = document.createElement('span');
        span.textContent = labelText;
        btn.appendChild(span);
        btn.addEventListener('mouseenter', function() { btn.style.background = bgHover; });
        btn.addEventListener('mouseleave', function() { btn.style.background = bg; });
        btn._span = span;
        return btn;
    }

    function sep() {
        var hr = document.createElement('hr');
        hr.style.cssText = 'border:none;border-top:1px solid rgba(0,0,0,0.08);margin:10px 0;';
        return hr;
    }

    function showModal(title, message, buttons) {
        var overlay = document.createElement('div');
        overlay.style.cssText = [
            'position:fixed;inset:0;z-index:10000001;',
            'background:rgba(0,0,0,0.45);',
            'backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);',
            'display:flex;align-items:center;justify-content:center;',
        ].join('');
        var card = document.createElement('div');
        card.style.cssText = [
            'background:rgba(255,255,255,0.92);',
            'backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);',
            'border:1px solid rgba(255,255,255,0.6);',
            'border-radius:16px;padding:24px 28px;',
            'box-shadow:0 16px 48px rgba(0,0,0,0.22);',
            'font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Arial,sans-serif;',
            'color:#1a1a2e;max-width:400px;min-width:260px;',
        ].join('');
        var titleEl = document.createElement('div');
        titleEl.textContent = title;
        titleEl.style.cssText = 'font-weight:700;font-size:14px;margin-bottom:10px;';
        card.appendChild(titleEl);
        var msgEl = document.createElement('div');
        msgEl.textContent = message;
        msgEl.style.cssText = 'font-size:13px;color:#444;white-space:pre-wrap;margin-bottom:18px;line-height:1.5;max-height:60vh;overflow-y:auto;';
        card.appendChild(msgEl);
        var btnRow = document.createElement('div');
        btnRow.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;';
        buttons.forEach(function (b) {
            var btn = document.createElement('button');
            btn.textContent = b.label;
            btn.style.cssText = [
                'padding:7px 18px;border:none;border-radius:8px;',
                'font-size:12.5px;font-weight:600;cursor:pointer;transition:opacity .15s;',
                'background:' + (b.primary ? '#4a90e2' : 'rgba(0,0,0,0.08)') + ';',
                'color:' + (b.primary ? '#fff' : '#1a1a2e') + ';',
            ].join('');
            btn.addEventListener('mouseenter', function () { btn.style.opacity = '0.8'; });
            btn.addEventListener('mouseleave', function () { btn.style.opacity = '1'; });
            btn.addEventListener('click', function () {
                if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
                if (b.action) b.action();
            });
            btnRow.appendChild(btn);
        });
        card.appendChild(btnRow);
        overlay.appendChild(card);
        document.body.appendChild(overlay);
    }

    function showAlert(title, message, onOk) {
        showModal(title, message, [{ label: 'OK', primary: true, action: onOk || null }]);
    }

    function showConfirm(title, message, onConfirm) {
        showModal(title, message, [
            { label: 'Abbrechen', primary: false },
            { label: 'Bestaetigen', primary: true, action: onConfirm },
        ]);
    }

    var lastUrl      = '';
    var panelBuilt   = false;
    var progressBar  = null;
    var activeInterval = null;

    setInterval(function () {
        var url = window.location.href;
        if (url === lastUrl) return;
        lastUrl = url;
        if (activeInterval) { clearInterval(activeInterval); activeInterval = null; }
        if (window.location.pathname.includes('/lessonDetails/')) {
            onLessonDetailsPage();
        } else {
            onTimetablePage();
        }
    }, 300);

    function onLessonDetailsPage() {
        if (!GM_getValue('scrapingMode', false)) return;
        var tries = 0;
        activeInterval = setInterval(function () {
            tries++;
            if (document.querySelector('.empty-indicator.empty-indicator--without-card')) {
                clearInterval(activeInterval); activeInterval = null;
                saveAndGoBack('');
                return;
            }
            var labels = document.querySelectorAll('.inline-text-input label');
            for (var i = 0; i < labels.length; i++) {
                if (labels[i].textContent.trim() === 'Lehrstoff') {
                    var container = labels[i].closest('.inline-text-input');
                    var ta = container && container.querySelector('textarea');
                    if (ta) {
                        clearInterval(activeInterval); activeInterval = null;
                        saveAndGoBack(ta.value || ta.textContent.trim());
                        return;
                    }
                }
            }
            if (tries >= 30) { clearInterval(activeInterval); activeInterval = null; saveAndGoBack(''); }
        }, 500);
    }

    function saveAndGoBack(lehrstoff) {
        if (lehrstoff && lehrstoff.trim()) {
            var subject = GM_getValue('currentSubject', '?');
            var results = JSON.parse(GM_getValue('scrapingResults', '[]'));
            var parts   = window.location.pathname.split('/');
            results.push({
                subject:   subject,
                lehrstoff: lehrstoff.trim(),
                startTime: decodeURIComponent(parts[7] || ''),
                endTime:   decodeURIComponent(parts[8] || '')
            });
            GM_setValue('scrapingResults', JSON.stringify(results));
        }
        history.back();
    }

    function onTimetablePage() {
        if (GM_getValue('scrapingMode', false)) {
            var idx   = GM_getValue('scrapingIndex', 0);
            var total = GM_getValue('scrapingTotal', 0);
            if (idx < total) {
                setProgressBar(idx, total);
                clickCard(idx);
            } else {
                GM_setValue('scrapingMode', false);
                removeProgressBar();
                rebuildPanel();
                var results = JSON.parse(GM_getValue('scrapingResults', '[]'));
                if (results.length > 0) {
                    setTimeout(function () {
                        var text = buildText(results);
                        showModal('Scraping fertig', results.length + ' Lektionen:\n\n' + text, [
                            {
                                label: 'Alles kopieren',
                                primary: true,
                                action: function () {
                                    navigator.clipboard.writeText(text);
                                }
                            },
                            { label: 'Schließen', primary: false }
                        ]);
                    }, 400);
                }
            }
        } else {
            if (!panelBuilt) { panelBuilt = true; buildTimetablePanel(); }
        }
    }

    function clickCard(idx) {
        var tries = 0;
        activeInterval = setInterval(function () {
            tries++;
            var cards = document.querySelectorAll('.timetable-grid-card .lesson-card.clickable');
            if (cards.length > idx) {
                clearInterval(activeInterval); activeInterval = null;
                var card  = cards[idx];
                var subEl = card.querySelector('[data-testid="lesson-card-resources-with-change-subject"] [data-testid="regular-resource"]');
                GM_setValue('currentSubject', subEl ? subEl.textContent.trim() : ('Lektion ' + (idx + 1)));
                GM_setValue('scrapingIndex', idx + 1);
                card.click();
            }
            if (tries > 40) {
                clearInterval(activeInterval); activeInterval = null;
                GM_setValue('scrapingMode', false);
                removeProgressBar();
                showAlert('Scraping abgebrochen', 'Karte ' + (idx + 1) + ' nicht gefunden.');
                rebuildPanel();
            }
        }, 500);
    }

    function setProgressBar(current, total) {
        if (!progressBar) {
            var overlay = document.createElement('div');
            overlay.style.cssText = [
                'position:fixed;inset:0;z-index:9999998;',
                'background:rgba(0,0,0,0.45);',
                'backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);',
            ].join('');
            var card = document.createElement('div');
            card.style.cssText = [
                'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);',
                'z-index:9999999;background:rgba(255,255,255,0.88);',
                'backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);',
                'border:1px solid rgba(255,255,255,0.6);border-radius:16px;padding:28px 32px;',
                'box-shadow:0 16px 48px rgba(0,0,0,0.22);',
                'font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Arial,sans-serif;',
                'color:#1a1a2e;text-align:center;min-width:220px;',
            ].join('');
            var spinWrap = document.createElement('div');
            spinWrap.style.cssText = 'display:flex;justify-content:center;margin-bottom:16px;';
            var spinSvg = icon(ICONS.loader, 36);
            spinSvg.style.cssText += 'animation:__scraper_spin 1s linear infinite;color:#4a90e2;';
            if (!document.getElementById('__scraper_keyframes')) {
                var style = document.createElement('style');
                style.id = '__scraper_keyframes';
                style.textContent = '@keyframes __scraper_spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}';
                document.head.appendChild(style);
            }
            spinWrap.appendChild(spinSvg);
            card.appendChild(spinWrap);
            var lbl = document.createElement('div');
            lbl.style.cssText = 'font-size:14px;font-weight:600;margin-bottom:6px;';
            card._label = lbl;
            card.appendChild(lbl);
            var sub = document.createElement('div');
            sub.style.cssText = 'font-size:12px;color:#888;margin-bottom:6px;';
            sub.textContent = 'Bitte verlassen Sie diesen Tab nicht…';
            card.appendChild(sub);
            var hint = document.createElement('div');
            hint.style.cssText = 'font-size:11px;color:#bbb;margin-bottom:18px;';
            hint.textContent = 'Sollten Sie nicht weiterkommen, drücken Sie bitte die ESC-Taste oder warten Sie ein bisschen.';
            card.appendChild(hint);
            var cancelBtn = document.createElement('button');
            cancelBtn.style.cssText = [
                'background:rgba(0,0,0,0.07);color:#1a1a2e;',
                'border:1px solid rgba(0,0,0,0.14);border-radius:8px;',
                'padding:6px 16px;font-size:12px;font-weight:600;cursor:pointer;',
                'display:inline-flex;align-items:center;gap:6px;transition:background .15s;',
            ].join('');
            cancelBtn.appendChild(icon(ICONS.xmark, 13));
            var cspan = document.createElement('span');
            cspan.textContent = 'Abbrechen';
            cancelBtn.appendChild(cspan);
            cancelBtn.addEventListener('mouseenter', function () { cancelBtn.style.background = 'rgba(0,0,0,0.13)'; });
            cancelBtn.addEventListener('mouseleave', function () { cancelBtn.style.background = 'rgba(0,0,0,0.07)'; });
            cancelBtn.addEventListener('click', function () {
                if (activeInterval) { clearInterval(activeInterval); activeInterval = null; }
                GM_setValue('scrapingMode', false);
                GM_setValue('scrapingResults', '[]');
                removeProgressBar();
                rebuildPanel();
            });
            card.appendChild(cancelBtn);
            overlay.appendChild(card);
            document.body.appendChild(overlay);
            progressBar = overlay;
            progressBar._label = lbl;
        }
        progressBar._label.textContent = 'Scraping ' + (current + 1) + ' / ' + total;
    }

    function removeProgressBar() {
        if (progressBar && progressBar.parentNode) {
            progressBar.parentNode.removeChild(progressBar);
            progressBar = null;
        }
    }

    function rebuildPanel() {
        ['__scraperToggle', '__scraperPanel'].forEach(function (id) {
            var el = document.getElementById(id);
            if (el) el.parentNode.removeChild(el);
        });
        panelBuilt = true;
        buildTimetablePanel();
    }

    function buildTimetablePanel() {
        var overlay = document.createElement('div');
        overlay.id = '__scraperPanel';
        overlay.style.cssText = [
            'position:fixed;inset:0;z-index:999999;',
            'background:rgba(0,0,0,0.45);',
            'backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);',
            'display:none;align-items:center;justify-content:center;',
        ].join('');
        overlay.addEventListener('click', function (e) {
            if (e.target === overlay) overlay.style.display = 'none';
        });

        var card = document.createElement('div');
        card.style.cssText = [
            'background:rgba(255,255,255,0.92);',
            'backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);',
            'border:1px solid rgba(255,255,255,0.6);',
            'border-radius:16px;padding:22px 24px 20px;',
            'box-shadow:0 16px 48px rgba(0,0,0,0.22);',
            'font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Arial,sans-serif;',
            'font-size:13px;color:#1a1a2e;min-width:260px;max-width:320px;',
            'position:relative;',
        ].join('');

        var xBtn = document.createElement('button');
        xBtn.title = 'Schließen';
        xBtn.style.cssText = [
            'position:absolute;top:12px;right:12px;',
            'background:rgba(0,0,0,0.06);border:none;border-radius:50%;',
            'width:26px;height:26px;cursor:pointer;padding:0;',
            'display:flex;align-items:center;justify-content:center;',
            'transition:background .15s;color:#555;',
        ].join('');
        xBtn.appendChild(icon(ICONS.xmark, 13));
        xBtn.addEventListener('mouseenter', function () { xBtn.style.background = 'rgba(0,0,0,0.13)'; });
        xBtn.addEventListener('mouseleave', function () { xBtn.style.background = 'rgba(0,0,0,0.06)'; });
        xBtn.addEventListener('click', function () { overlay.style.display = 'none'; });
        card.appendChild(xBtn);

        var helpBtn = document.createElement('button');
        helpBtn.title = 'Info';
        helpBtn.style.cssText = [
            'position:absolute;top:12px;right:46px;',
            'background:rgba(0,0,0,0.06);border:none;border-radius:50%;',
            'width:26px;height:26px;cursor:pointer;padding:0;',
            'display:flex;align-items:center;justify-content:center;',
            'transition:background .15s;color:#555;',
            'font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Arial,sans-serif;',
            'font-size:13px;font-weight:700;',
        ].join('');
        helpBtn.textContent = '?';
        helpBtn.addEventListener('mouseenter', function () { helpBtn.style.background = 'rgba(0,0,0,0.13)'; });
        helpBtn.addEventListener('mouseleave', function () { helpBtn.style.background = 'rgba(0,0,0,0.06)'; });
        helpBtn.addEventListener('click', function () {
            showAlert('WebUntis Topic Scraper',
                'Eine inoffizielle Erweiterung für WebUntis, die die Lehrstoffe und Themen aus allen Unterrichtsstunden der jeweiligen Schulwoche automatisch herausliest.\n\n' +
                'So funktioniert es:\n' +
                '1. Zur gewünschten Schulwoche in WebUntis wechseln.\n' +
                '2. Den WebUntis Topic Scraper öffnen und auf "Scrapen starten" klicken.\n' +
                '3. Das Script klickt automatisch jede Unterrichtsstunde durch und liest den Lehrstoff aus.\n' +
                '4. Die Ergebnisse können danach kopiert und ins Berichtsheft eingefügt werden.\n\n' +
                'Projekt umgesetzt von Whiztech.'
            );
        });
        card.appendChild(helpBtn);

        //Title
        var titleDiv = document.createElement('div');
        titleDiv.textContent = 'WebUntis Topic Scraper';
        titleDiv.style.cssText = 'font-weight:700;font-size:13px;margin-bottom:14px;color:#1a1a2e;letter-spacing:0.01em;padding-right:60px;';
        card.appendChild(titleDiv);

        //Scrape button
        var scrapeBtn = makeBtn('Scrapen starten', 'play', '#4a90e2', '#357abd');
        scrapeBtn.style.marginBottom = '0';
        scrapeBtn.addEventListener('click', function () {
            var cards = document.querySelectorAll('.timetable-grid-card .lesson-card.clickable');
            if (cards.length === 0) { showAlert('Hinweis', 'Keine Lektionen gefunden.'); return; }
            GM_setValue('scrapingMode', true);
            GM_setValue('scrapingTotal', cards.length);
            GM_setValue('scrapingIndex', 1);
            GM_setValue('scrapingResults', '[]');
            var firstSubEl = cards[0].querySelector('[data-testid="lesson-card-resources-with-change-subject"] [data-testid="regular-resource"]');
            GM_setValue('currentSubject', firstSubEl ? firstSubEl.textContent.trim() : 'Lektion 1');
            overlay.style.display = 'none';
            setProgressBar(0, cards.length);
            cards[0].click();
        });
        card.appendChild(scrapeBtn);

        //Stored results section
        var stored = JSON.parse(GM_getValue('scrapingResults', '[]'));
        if (stored.length > 0) {
            card.appendChild(sep());
            var lbl = document.createElement('div');
            lbl.textContent = stored.length + ' Ergebnisse gespeichert';
            lbl.style.cssText = 'font-size:11.5px;color:#666;margin-bottom:8px;';
            card.appendChild(lbl);

            var clipBtn = makeBtn('Alles kopieren', 'copy', '#2ecc71', '#27ae60');
            clipBtn.addEventListener('click', function () {
                navigator.clipboard.writeText(buildText(stored)).then(function () {
                    clipBtn._span.textContent = 'Kopiert!';
                    clipBtn.replaceChild(icon(ICONS.check, 14), clipBtn.firstChild);
                    clipBtn.style.background = '#1a7a44';
                    setTimeout(function () {
                        clipBtn._span.textContent = 'Alles kopieren';
                        clipBtn.replaceChild(icon(ICONS.copy, 14), clipBtn.firstChild);
                        clipBtn.style.background = '#2ecc71';
                    }, 2000);
                });
            });
            card.appendChild(clipBtn);

            var showBtn = makeBtn('Ergebnisse anzeigen', 'eye', '#f39c12', '#d68910');
            showBtn.addEventListener('click', function () {
                showAlert('Gespeicherte Ergebnisse', stored.length + ' Lektionen:\n\n' + buildText(stored));
            });
            card.appendChild(showBtn);

            var clearBtn = makeBtn('Speicher leeren', 'trash', '#e74c3c', '#c0392b');
            clearBtn.style.marginBottom = '0';
            clearBtn.addEventListener('click', function () {
                showConfirm('Speicher leeren', 'Gespeicherte Ergebnisse wirklich löschen?', function () {
                    GM_setValue('scrapingResults', '[]');
                    rebuildPanel();
                });
            });
            card.appendChild(clearBtn);
        }

        overlay.appendChild(card);
        document.body.appendChild(overlay);

        var headerPollInterval = setInterval(function () {
            var controls = document.querySelector('.timetable-header-controls');
            if (!controls) return;
            if (document.getElementById('__scraperToggle')) return;
            clearInterval(headerPollInterval);

            var toggleBtn = document.createElement('button');
            toggleBtn.id = '__scraperToggle';
            toggleBtn.title = 'WebUntis Topic Scraper';
            toggleBtn.style.cssText = [
                'background:#4a90e2;color:#fff;border:none;border-radius:6px;',
                'padding:0 10px;height:28px;cursor:pointer;margin-right:8px;',
                'box-shadow:0 2px 8px rgba(0,0,0,0.12);transition:opacity .2s;',
                'display:inline-flex;align-items:center;gap:6px;',
                'font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Arial,sans-serif;',
                'font-size:12px;font-weight:600;vertical-align:middle;',
            ].join('');
            toggleBtn.appendChild(icon(ICONS.clipboard, 14));
            var tlbl = document.createElement('span');
            tlbl.textContent = 'Scraper';
            toggleBtn.appendChild(tlbl);
            toggleBtn.addEventListener('mouseenter', function () { toggleBtn.style.opacity = '0.82'; });
            toggleBtn.addEventListener('mouseleave', function () { toggleBtn.style.opacity = '1'; });
            controls.insertBefore(toggleBtn, controls.firstChild);

            toggleBtn.addEventListener('click', function () {
                overlay.style.display = overlay.style.display === 'flex' ? 'none' : 'flex';
            });
        }, 200);
    }

    function buildText(results) {
        var grouped = {};
        var order = [];
        results.forEach(function (r) {
            var key = r.subject || '?';
            if (!grouped[key]) { grouped[key] = []; order.push(key); }
            grouped[key].push(r.lehrstoff);
        });
        return order.map(function (key) {
            return key + ': ' + grouped[key].join('; ');
        }).join('\n');
    }

})();