Greasy Fork is available in English.

Advanced Twins for University of Tsukuba

Provide Advanced function for Twins (University of Tsukuba)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         Advanced Twins for University of Tsukuba
// @namespace    https://github.com/refiaa
// @version      240429.1605
// @description  Provide Advanced function for Twins (University of Tsukuba)
// @author       refiaa
// @match        https://twins.tsukuba.ac.jp/campusweb/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    class KeyObserver {
        constructor(targetSelector) {
            this.targetSelector = targetSelector;
            this.currentKey = '';
            this.latestKey = '';
            this.displayElement = null;
            this.logElement = null;
            this.inputField = null;
            this.addButton = null;
            this.targetIframe = null;
            this.init();

            this.initSessionStorage();
            this.logAnalyzer = new LogAnalyzer();
        }

        init() {
            this.findTargetIframe();
            if (this.targetIframe) {
                this.initObserver();
                this.initUIComponents();
            }
        }

        // SESSION STORAGE LOGIC

        initSessionStorage() {
            const logKey = 'flowExecutionLogs';
            if (!sessionStorage.getItem(logKey)) {
                sessionStorage.setItem(logKey, JSON.stringify([]));
            }
        }

        addLog(type, key, error = null) {
            const logKey = 'flowExecutionLogs';
            const currentLogs = JSON.parse(sessionStorage.getItem(logKey));

            const log = {
                type: type,
                key: key,
                timestamp: new Date().toISOString(),
                error: error
            };

            currentLogs.push(log);
            sessionStorage.setItem(logKey, JSON.stringify(currentLogs));
        }

        // END OF SESSION STORAGE LOGIC

        findTargetIframe() {
            const iframes = document.getElementsByTagName('iframe');
            for (const iframe of iframes) {
                if (iframe.src.includes('campussquare.do?_flowId=RSW0001000-flow')) {
                    this.targetIframe = iframe;
                    console.log('Target iframe found:', this.targetIframe);
                    break;
                }
            }
            if (!this.targetIframe) {
                console.warn('Target iframe not found');
            }
        }

        initObserver() {
            if (!this.targetIframe) {
                console.error('Target iframe not found');
                return;
            }

            try {
                const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document;
                const targetNode = targetDocument.querySelector(this.targetSelector);
                if (!targetNode) {
                    console.error('Target element not found in the iframe');
                    return;
                }

                const config = { attributes: true, childList: true, subtree: true };
                const observer = new MutationObserver(mutations => this.handleMutations(mutations));
                observer.observe(targetNode, config);
            } catch (error) {
                console.error('Error accessing iframe:', error);
            }
        }

        handleMutations(mutations) {
            mutations.forEach(mutation => {
                if (mutation.type === 'childList' || mutation.type === 'attributes') {
                    this.handleMutation(mutation);
                }
            });
        }

        handleMutation(mutation) {
            try {
                const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document;
                const target = targetDocument.querySelector('input[name="_flowExecutionKey"]');
                if (target && target.value !== this.currentKey) {
                    this.currentKey = target.value;
                    this.latestKey = this.currentKey;

                    this.addLog('childList', this.currentKey);
                }
            } catch (error) {
                this.addLog('error', 'handleMutation', error.message);
                console.error('Error handling mutation:', error);
            }
        }

        initUIComponents() {
            this.createDisplay();
            this.setupInputField();
            this.setupButtons();
        }

        createDisplay() {
            try {
                const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document;
                const uiPanel = targetDocument.createElement('div');
                uiPanel.id = 'keyObserverUI';
                uiPanel.classList.add('key-observer-panel');

                const targetParagraph = targetDocument.querySelector('table[border="0"][cellspacing="1"][cellpadding="1"] + p');
                if (targetParagraph) {
                    targetParagraph.parentNode.insertBefore(uiPanel, targetParagraph);
                    console.log('UI panel created:', uiPanel);
                } else {
                    console.warn('Target paragraph not found, appending UI panel to body');
                    targetDocument.body.appendChild(uiPanel);
                }

                this.displayElement = uiPanel;
                this.logElement = targetDocument.createElement('div');
                this.logElement.classList.add('key-observer-log');
                this.displayElement.appendChild(this.logElement);
            } catch (error) {
                console.error('Error creating display:', error);
            }
        }

        setupInputField() {
            try {
                const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document;
                this.inputField = targetDocument.createElement('input');
                this.inputField.id = 'jikanwariCodeInput';
                this.inputField.type = 'text';
                this.inputField.placeholder = '時間割コード';
                this.inputField.classList.add('jikanwari-code-input');
                this.displayElement.appendChild(this.inputField);
            } catch (error) {
                console.error('Error setting up input field:', error);
            }
        }

        setupButtons() {
            const actions = ['Add'];
            actions.forEach(action => this.createButton(action));
        }

        createButton(action) {
            try {
                const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document;
                const button = targetDocument.createElement('button');
                button.textContent = action;
                button.classList.add('key-observer-button');
                button.addEventListener('click', () => this.executeCommand(action));
                this.displayElement.appendChild(button);

                if (action === 'Add') {
                    this.addButton = button;
                }
            } catch (error) {
                console.error('Error creating button:', error);
            }
        }

        executeCommand(action, jikanwariShozokuCode, yobi, jigen, jikanwariCode) {
            if (action === 'Add') {
                jikanwariCode = this.inputField.value.trim();
                if (!jikanwariCode) {
                    alert('時間割コードを入力してください。');
                    return;
                }
            }

            const config = this.getButtonConfig(action, jikanwariShozokuCode, yobi, jigen);

            const finalizeAction = () => {
                this.sendRequest('back', this.getButtonConfig('back'))
                    .then(() => {
                        this.refreshTimetable();
                        if (action === 'Add') {
                            this.clearInputField();
                        }
                    })
                    .catch(error => {
                        console.error('Error during finalization:', error);
                    });
            };

            if (action === 'Add') {
                this.sendRequest('input', { ...config.inputParams, jikanwariCode })
                    .then(() => {
                        this.sendRequest('insert', { nendo: new Date().getFullYear().toString(), jikanwariCode })
                            .then(finalizeAction)
                            .catch(error => {
                                console.error('Error during the insert process:', error);
                                finalizeAction();
                            });
                    })
                    .catch(error => {
                        console.error('Error during the first request in insert with input:', error);
                        finalizeAction();
                    });
            } else if (action === 'delete') {
                this.sendRequest('delete', { ...config, jikanwariCode })
                    .then(() => {
                        this.sendRequest('delete', { _flowExecutionKey: this.latestKey || this.currentKey })
                            .then(finalizeAction)
                            .catch(error => {
                                console.error('Error during the delete process:', error);
                                finalizeAction();
                            });
                    })
                    .catch(error => {
                        console.error('Error during the delete process:', error);
                        finalizeAction();
                    });
            }
        }

        clearInputField() {
            this.inputField.value = '';
        }

        getButtonConfig(action, jikanwariShozokuCode, yobi, jigen) {
            const configs = {
                add: { inputParams: { yobi: 9, jigen: 0 } },
                delete: { nendo: new Date().getFullYear().toString(), jikanwariShozokuCode, yobi, jigen },
                back: {}
            };
            return configs[action] || {};
        }

        sendRequest(eventId, params) {
            const keyToUse = this.latestKey || this.currentKey;
            const requestOptions = {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: this.encodeParams({ ...params, _flowExecutionKey: keyToUse, _eventId: eventId })
            };

            return fetch(`/campusweb/campussquare.do`, requestOptions)
                .then((response) => response.text())
                .then((html) => {
                    this.handleResponse(html, eventId);

                    this.logAnalyzer.analyzeLogs();
                })
                .catch((error) => {
                    console.error('Error with AJAX request:', error);
                });
        }

        handleResponse(html, eventId) {
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');
            const newKey = doc.querySelector('input[name="_flowExecutionKey"]')?.value;

            if (newKey) {
                this.latestKey = newKey;
                this.addLog(eventId, newKey);
            } else {
                throw new Error('Failed to fetch new key');
            }
        }

        encodeParams(params) {
            return Object.keys(params)
                .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(params[key]))
                .join('&');
        }

        refreshTimetable()   {
            return new Promise((resolve, reject) => {
                const requestOptions = {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body: this.encodeParams({ _flowExecutionKey: this.latestKey || this.currentKey })
                };

                fetch(`/campusweb/campussquare.do?_flowId=RSW0001000-flow`, requestOptions)
                    .then(response => response.text())
                    .then(html => {
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(html, 'text/html');
                        const newTimetable = doc.querySelector('table.rishu-koma');
                        if (newTimetable) {
                            const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document;
                            const currentTimetable = targetDocument.querySelector('table.rishu-koma');
                            if (currentTimetable) {
                                currentTimetable.parentNode.replaceChild(newTimetable, currentTimetable);
                                resolve();
                            } else {
                                reject('Current timetable not found');
                            }
                        } else {
                            reject('New timetable not found');
                        }
                    })
                    .catch(error => {
                        console.error('Error refreshing timetable:', error);
                        reject(error);
                    });
            });
        }
    }

    class AdvancedSyllabus {
        constructor(keyObserver) {
            this.keyObserver = keyObserver;
            this.targetIframe = keyObserver.targetIframe;
            this.init();
        }

        init() {
            this.injectStyles();
            this.observePageChanges();
        }

        injectStyles() {
            const styleElement = document.createElement('style');
            styleElement.textContent = `
                .syllabus-link {
                    color: blue !important;
                    cursor: pointer !important;
                    text-decoration: underline !important;
                    margin-left: 5px;
                }
                .delete-button {
                    background-color: transparent;
                    font-weight: bold;
                    color: #333;
                    border: none;
                    border-radius: 0;
                    width: 24px;
                    height: 24px;
                    position: absolute;
                    top: 10px;
                    right: 10px;
                    transform: translate(50%, -50%);
                    cursor: pointer;
                    z-index: 1;
                }

                .delete-button:hover {
                    color: #ff0000;
                    text-decoration: none;
                }

                .delete-button::before {
                    content: "科目を削除";
                    visibility: hidden;
                    color: #fff;
                    background-color: #555;
                    padding: 5px 10px;
                    border-radius: 6px;
                    position: absolute;
                    z-index: 1000;
                    left: 120%;
                    top: -75%;
                    transform: translate(-50%, -50%);
                    white-space: nowrap;
                    font-size: 12px;
                    box-shadow: 0px 2px 5px rgba(0,0,0,0.2);
                    transition: visibility 0.2s, opacity 0.2s ease;
                    opacity: 0;
                }

                .delete-button:hover::before {
                    visibility: visible;
                    opacity: 1;
                }
            `;

            try {
                const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document;
                targetDocument.head.appendChild(styleElement);
            } catch (error) {
                console.error('Error injecting styles:', error);
            }
        }

        observePageChanges() {
            const targetNode = document.body;
            const observerOptions = {
                childList: true,
                subtree: true
            };

            const observer = new MutationObserver(mutationsList => {
                for (const mutation of mutationsList) {
                    if (mutation.type === 'childList') {
                        this.addLinksToTimetable();
                    }
                }
            });

            observer.observe(targetNode, observerOptions);
        }

        addLinksToTimetable() {
            if (!this.targetIframe) {
                console.warn('Target iframe not found, skipping adding links to timetable');
                return;
            }

            try {
                const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document;
                const timetableCells = targetDocument.querySelectorAll('td[bgcolor="#ffcc99"], td[bgcolor="#ffffcc"]');
                timetableCells.forEach(cell => {
                    this.addLinkToCell(cell, false);
                    this.addDeleteButton(cell);
                });

                const concentratedRows = targetDocument.querySelectorAll('table.rishu-etc tr[bgcolor="#ffcc99"]');
                concentratedRows.forEach(row => {
                    const syllabusCell = row.children[4];
                    if (syllabusCell) {
                        this.addLinkToCell(syllabusCell, true);
                        this.addDeleteButton(syllabusCell);
                    }
                });
            } catch (error) {
                console.error('Error adding links to timetable:', error);
            }
        }

        addLinkToCell(cell, isSyllabus) {
            if (cell.querySelector('.syllabus-link')) return;

            let courseCode = '';

            if (isSyllabus) {
                const courseCodeCell = cell.parentElement.querySelector('td:nth-child(3)');
                if (courseCodeCell) {
                    courseCode = courseCodeCell.textContent.trim();
                }
            } else {
                const courseCodeElement = cell.querySelector('td[valign="top"]');
                if (courseCodeElement) {
                    const textContent = courseCodeElement.textContent.trim();
                    const lines = textContent.split('\n');
                    courseCode = lines[0].trim();
                }
            }

            if (!courseCode) return;

            const link = document.createElement('a');
            link.className = 'syllabus-link';
            link.textContent = 'シラバス';
            link.href = `https://kdb.tsukuba.ac.jp/syllabi/${new Date().getFullYear()}/${courseCode}/jpn`;
            link.target = '_blank';
            link.rel = 'noopener noreferrer';
            link.addEventListener('click', function (event) {
                event.preventDefault();
                window.open(this.href, 'syllabusWindow', 'width=800,height=600,resizable=yes,scrollbars=yes');
            });

            const linkContainer = document.createElement('div');
            linkContainer.classList.add('syllabus-link-container');
            linkContainer.appendChild(link);

            if (!cell.querySelector('.syllabus-link-container')) {
                cell.appendChild(linkContainer);
            }
        }

        addDeleteButton(cell) {
            if (cell.querySelector('.delete-button')) return;

            const deleteButton = document.createElement('button');
            deleteButton.classList.add('delete-button');
            deleteButton.innerHTML = '✕';
            deleteButton.addEventListener('click', () => {
                const deleteLink = cell.querySelector('a[onclick^="return DeleteCallA"]');
                if (deleteLink) {
                    const onclickArgs = deleteLink.getAttribute('onclick').match(/'(.*?)'/g);
                    if (onclickArgs && onclickArgs.length === 5) {
                        const [nendo, jikanwariShozokuCode, jikanwariCode, yobi, jigen] = onclickArgs.map(arg => arg.replace(/'/g, ''));
                        const confirmDelete = window.confirm('本当にこの科目を削除しますか?');
                        if (confirmDelete) {
                            this.keyObserver.executeCommand('delete', jikanwariShozokuCode, yobi, jigen, jikanwariCode);
                        }
                    }
                }
            });

            const deleteButtonContainer = document.createElement('div');
            deleteButtonContainer.classList.add('delete-button-container');
            deleteButtonContainer.appendChild(deleteButton);

            cell.style.position = 'relative';
            cell.appendChild(deleteButtonContainer);
        }
    }

    class kdb_Displayer {
        constructor(urls, keyObserver) {
            this.urls = urls;

            this.keyObserver = keyObserver;
            if (!this.keyObserver) {
                console.error('KeyObserver not initialized');
                return;
            }

            this.displayElement = null;
            this.toggleButton = null;
            this.searchContainer = null;
            this.sortContainer = null;
            this.tableContainer = null;
            this.jsonData = null;
            this.indexedData = null;
            this.filteredData = null;
            this.dayOfWeekSelect = null;
            this.periodSelect = null;
            this.currentPage = 1;
            this.pageSize = 20;
            this.currentUrlIndex = 0;

            this.init();
        }

        async init() {
            try {
                const targetIframe = await this.waitForTargetIframe();
                if (targetIframe && this.keyObserver) {
                    const jsonData = await this.fetchJsonData(this.urls[this.currentUrlIndex]);
                    this.createUI();
                    this.displayJsonData(jsonData);
                } else {
                    console.warn('KeyObserver not initialized or target iframe not found');
                }
            } catch (error) {
                console.error('JSON Error', error);
            }
        }

        async waitForTargetIframe(maxRetries = 10, retryDelay = 1000) {
            let retries = 0;
            return new Promise((resolve, reject) => {
                const checkIframe = () => {
                    const targetIframe = document.querySelector('iframe[src*="campussquare.do?_flowId=RSW0001000-flow"]');
                    if (targetIframe) {
                        resolve(targetIframe);
                    } else {
                        retries++;
                        if (retries < maxRetries) {
                            setTimeout(checkIframe, retryDelay);
                        } else {
                            resolve(null);
                        }
                    }
                };
                checkIframe();
            });
        }

        async fetchJsonData(url) {
            const response = await fetch(url);
            if (!response.ok) {
                throw new Error(`HTTP error status: ${response.status}`);
            }
            return await response.json();
        }

        createUI() {
            const existingElement = document.getElementById('kdb_Displayer');
            const existingToggleButton = document.getElementById('jsonToggleButton');

            if (existingElement) {
                this.displayElement = existingElement;
                this.toggleButton = existingToggleButton;
                this.searchContainer = document.getElementById('searchContainer');
                this.sortContainer = document.getElementById('sortContainer');
                this.tableContainer = document.getElementById('tableContainer');
            } else {
                this.displayElement = document.createElement('div');
                this.displayElement.id = 'kdb_Displayer';
                this.displayElement.style.position = 'fixed';
                this.displayElement.style.bottom = '2vh';
                this.displayElement.style.right = '2vw';
                this.displayElement.style.width = '40vw';
                this.displayElement.style.maxHeight = '60vh';
                this.displayElement.style.backgroundColor = 'rgba(255, 255, 255, 0.9)';
                this.displayElement.style.color = 'black';
                this.displayElement.style.padding = '1vw';
                this.displayElement.style.zIndex = '9999';
                this.displayElement.style.overflowY = 'auto';
                this.displayElement.style.border = '1px solid #ccc';
                this.displayElement.style.borderRadius = '5px';
                this.displayElement.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.1)';
                this.displayElement.style.display = 'none';

                // Header
                const headerDiv = document.createElement('div');
                headerDiv.style.display = 'flex';
                headerDiv.style.alignItems = 'center';
                headerDiv.style.marginBottom = '10px';

                const mainHeaderText = document.createElement('span');
                mainHeaderText.style.fontWeight = 'bold';
                mainHeaderText.style.fontSize = '25px';
                mainHeaderText.textContent = 'KDB Searcher';

                const subHeaderText = document.createElement('span');
                subHeaderText.style.fontSize = '14px';
                subHeaderText.style.marginLeft = '10px';

                const subHeaderTextNode = document.createTextNode('Source code is available on ');
                subHeaderText.appendChild(subHeaderTextNode);

                const githubLink = document.createElement('a');
                githubLink.href = 'https://github.com/refiaa';
                githubLink.target = '_blank';
                githubLink.textContent = 'Github';

                subHeaderText.appendChild(githubLink);

                headerDiv.appendChild(mainHeaderText);
                headerDiv.appendChild(subHeaderText);
                this.displayElement.appendChild(headerDiv);

                this.toggleButton = document.createElement('button');
                this.toggleButton.id = 'jsonToggleButton';
                this.toggleButton.textContent = 'kdbを開く';
                this.toggleButton.style.position = 'fixed';
                this.toggleButton.style.bottom = '20px';
                this.toggleButton.style.right = '20px';
                this.toggleButton.style.padding = '5px 10px';
                this.toggleButton.style.zIndex = '9999';
                this.toggleButton.style.cursor = 'pointer';

                this.toggleButton.addEventListener('click', this.toggleDisplay.bind(this));

                this.searchContainer = document.createElement('div');
                this.searchContainer.id = 'searchContainer';
                this.sortContainer = document.createElement('div');
                this.sortContainer.id = 'sortContainer';
                this.tableContainer = document.createElement('div');
                this.tableContainer.id = 'tableContainer';

                this.createSearchUI();
                this.createSortUI();
                this.createUrlSwitchUI();

                const spacerDiv1 = document.createElement('div');
                spacerDiv1.style.marginBottom = '15px';
                this.displayElement.appendChild(spacerDiv1);

                this.displayElement.appendChild(this.searchContainer);
                this.displayElement.appendChild(this.sortContainer);
                this.displayElement.appendChild(this.tableContainer);

                // レファレンス
                const descriptionDiv = document.createElement('div');
                descriptionDiv.style.marginTop = '20px';
                descriptionDiv.style.fontSize = '14px';
                descriptionDiv.style.color = '#666';

                const descriptionTextNode1 = document.createTextNode('Using ');
                descriptionDiv.appendChild(descriptionTextNode1);

                const kdbLink = document.createElement('a');
                kdbLink.href = 'https://github.com/Make-IT-TSUKUBA/alternative-tsukuba-kdb';
                kdbLink.target = '_blank';
                kdbLink.textContent = 'alternative-tsukuba-kdb';
                descriptionDiv.appendChild(kdbLink);

                const descriptionTextNode2 = document.createTextNode(' for kdb data.');
                descriptionDiv.appendChild(descriptionTextNode2);

                this.displayElement.appendChild(descriptionDiv);

                document.body.appendChild(this.displayElement);
                document.body.appendChild(this.toggleButton);
            }
        }

        createSearchUI() {
            const searchUIContainer = document.createElement('div');
            searchUIContainer.style.marginBottom = '20px';

            const subjectSearchRow = document.createElement('div');
            subjectSearchRow.classList.add('row')

            const subjectCodeInput = document.createElement('input');
            subjectCodeInput.type = 'text';
            subjectCodeInput.placeholder = '科目番号で検索';
            subjectCodeInput.style.marginRight = '10px';
            subjectCodeInput.addEventListener('input', this.filterData.bind(this));
            subjectSearchRow.appendChild(subjectCodeInput);

            const subjectNameInput = document.createElement('input');
            subjectNameInput.type = 'text';
            subjectNameInput.placeholder = '科目名で検索';
            subjectNameInput.style.marginRight = '10px';
            subjectNameInput.addEventListener('input', this.filterData.bind(this));
            subjectSearchRow.appendChild(subjectNameInput);

            this.searchContainer.appendChild(subjectSearchRow);

            const dayTimeRow = document.createElement('div');
            dayTimeRow.classList.add('row');

            const daysOfWeekOptions = ['月', '火', '水', '木', '金', '土', '日'];
            this.dayOfWeekSelect = document.createElement('select');

            const daysOfWeekPlaceholderOption = document.createElement('option');
            daysOfWeekPlaceholderOption.value = '';
            daysOfWeekPlaceholderOption.textContent = '曜日';
            daysOfWeekPlaceholderOption.disabled = true;
            daysOfWeekPlaceholderOption.selected = true;
            this.dayOfWeekSelect.appendChild(daysOfWeekPlaceholderOption);

            for (const option of daysOfWeekOptions) {
                const daysOfWeekOption = document.createElement('option');
                daysOfWeekOption.value = option;
                daysOfWeekOption.textContent = option;
                this.dayOfWeekSelect.appendChild(daysOfWeekOption);
            }

            this.dayOfWeekSelect.addEventListener('change', this.filterData.bind(this));
            dayTimeRow.appendChild(this.dayOfWeekSelect);

            const periodsOptions = ['1', '2', '3', '4', '5', '6', '7', '8'];
            this.periodSelect = document.createElement('select');

            const periodPlaceholderOption = document.createElement('option');
            periodPlaceholderOption.value = '';
            periodPlaceholderOption.textContent = '時限';
            periodPlaceholderOption.disabled = true;
            periodPlaceholderOption.selected = true;
            this.periodSelect.appendChild(periodPlaceholderOption);

            for (const option of periodsOptions) {
                const periodsOption = document.createElement('option');
                periodsOption.value = option;
                periodsOption.textContent = option;
                this.periodSelect.appendChild(periodsOption);
            }

            this.periodSelect.addEventListener('change', this.filterData.bind(this));
            dayTimeRow.appendChild(this.periodSelect);

            this.searchContainer.appendChild(dayTimeRow);

            const filterRow = document.createElement('div');
            filterRow.classList.add('row');

            const semesterContainer = document.createElement('div');
            const semesterOptions = ['春', '秋', 'A', 'B', 'C'];
            for (const option of semesterOptions) {
                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.value = option;
                checkbox.id = `semester-${option}`;
                checkbox.style.marginRight = '5px';
                checkbox.addEventListener('change', this.filterData.bind(this));
                semesterContainer.appendChild(checkbox);

                const label = document.createElement('label');
                label.htmlFor = `semester-${option}`;
                label.textContent = option;
                semesterContainer.appendChild(label);
            }

            filterRow.appendChild(semesterContainer);

            const onlineOfflineContainer = document.createElement('div');
            const onlineOfflineOptions = ['オンライン', '対面'];
            for (const option of onlineOfflineOptions) {
                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.value = option;
                checkbox.id = `format-${option}`;
                checkbox.style.marginRight = '5px';
                checkbox.addEventListener('change', this.filterData.bind(this));
                onlineOfflineContainer.appendChild(checkbox);

                const label = document.createElement('label');
                label.htmlFor = `format-${option}`;
                label.textContent = option;
                onlineOfflineContainer.appendChild(label);
            }

            filterRow.appendChild(onlineOfflineContainer);

            const resetButton = document.createElement('button');
            resetButton.textContent = '検索結果をリセット';
            resetButton.style.marginLeft = '80px';
            resetButton.addEventListener('click', this.resetSearchOptions.bind(this));
            onlineOfflineContainer.appendChild(resetButton);

            this.searchContainer.appendChild(filterRow);

            const style = document.createElement('style');
            style.textContent = `.row {margin-bottom: 10px;}`;
            document.head.appendChild(style);
        }

        resetSearchOptions() {
            const subjectCodeInput = this.displayElement.querySelector('input[placeholder="科目番号で検索"]');
            const subjectNameInput = this.displayElement.querySelector('input[placeholder="科目名で検索"]');
            const semesterCheckboxes = this.displayElement.querySelectorAll('input[id^="semester-"]');
            const onlineOfflineCheckboxes = this.displayElement.querySelectorAll('input[id^="format-"]');

            subjectCodeInput.value = '';
            subjectNameInput.value = '';
            this.dayOfWeekSelect.selectedIndex = 0;
            this.periodSelect.selectedIndex = 0;
            semesterCheckboxes.forEach(checkbox => checkbox.checked = false);
            onlineOfflineCheckboxes.forEach(checkbox => checkbox.checked = false);

            this.filterData();
        }

        createSortUI() {
            const sortLabel = document.createElement('label');
            sortLabel.textContent = '並び変え: ';
            this.sortContainer.appendChild(sortLabel);

            const sortSelect = document.createElement('select');
            sortSelect.style.marginRight = '10px';
            const sortOptions = [
                {value: 'subjectCode', label: '科目番号'},
                {value: 'subjectName', label: '科目名'},
            ];
            for (const option of sortOptions) {
                const sortOption = document.createElement('option');
                sortOption.value = option.value;
                sortOption.textContent = option.label;
                sortSelect.appendChild(sortOption);
            }
            sortSelect.addEventListener('change', this.sortData.bind(this));
            this.sortContainer.appendChild(sortSelect);

            const sortOrderSelect = document.createElement('select');
            const sortOrderOptions = [
                {value: 'asc', label: '昇順'},
                {value: 'desc', label: '降順'},
            ];
            for (const option of sortOrderOptions) {
                const sortOrderOption = document.createElement('option');
                sortOrderOption.value = option.value;
                sortOrderOption.textContent = option.label;
                sortOrderSelect.appendChild(sortOrderOption);
            }
            sortOrderSelect.addEventListener('change', this.sortData.bind(this));

            this.sortContainer.style.marginBottom = '15px'
            this.sortContainer.appendChild(sortOrderSelect);
        }

        toggleDisplay() {
            if (this.displayElement.style.display === 'none') {
                this.displayElement.style.display = 'block';
            } else {
                this.displayElement.style.display = 'none';
            }
        }

        createUrlSwitchUI() {
            const urlSwitchContainer = document.createElement('div');
            urlSwitchContainer.style.marginTop = '10px';
            urlSwitchContainer.style.display = 'flex';
            urlSwitchContainer.style.alignItems = 'center';

            const toggleSwitch = document.createElement('div');
            toggleSwitch.style.position = 'relative';
            toggleSwitch.style.display = 'inline-block';
            toggleSwitch.style.width = '40px';
            toggleSwitch.style.height = '20px';
            toggleSwitch.style.borderRadius = '20px';
            toggleSwitch.style.backgroundColor = '#ccc';
            toggleSwitch.style.cursor = 'pointer';
            toggleSwitch.style.transition = 'background-color 0.3s';

            const toggleIndicator = document.createElement('div');
            toggleIndicator.style.position = 'absolute';
            toggleIndicator.style.top = '2px';
            toggleIndicator.style.left = '2px';
            toggleIndicator.style.width = '16px';
            toggleIndicator.style.height = '16px';
            toggleIndicator.style.borderRadius = '50%';
            toggleIndicator.style.backgroundColor = 'white';
            toggleIndicator.style.boxShadow = '0 1px 2px rgba(0, 0, 0, 0.2)';
            toggleIndicator.style.transition = 'transform 0.3s';

            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.style.display = 'none';

            checkbox.addEventListener('change', () => {
                this.currentUrlIndex = checkbox.checked ? 1 : 0;

                this.resetSearchOptions();

                this.fetchJsonData(this.urls[this.currentUrlIndex]).then((jsonData) => {
                    this.displayJsonData(jsonData);
                });

                toggleIndicator.style.transform = checkbox.checked ? 'translateX(20px)' : 'translateX(0)';
                toggleSwitch.style.backgroundColor = checkbox.checked ? '#2196F3' : '#ccc';
            });

            toggleSwitch.appendChild(checkbox);
            toggleSwitch.appendChild(toggleIndicator);

            const labelUrl1 = document.createElement('span');
            labelUrl1.textContent = '学類';
            labelUrl1.style.marginRight = '5px';
            labelUrl1.style.fontSize = '14px';

            const labelUrl2 = document.createElement('span');
            labelUrl2.textContent = '大学院';
            labelUrl2.style.marginLeft = '5px';
            labelUrl2.style.fontSize = '14px';

            urlSwitchContainer.appendChild(labelUrl1);
            urlSwitchContainer.appendChild(toggleSwitch);
            urlSwitchContainer.appendChild(labelUrl2);

            toggleSwitch.addEventListener('click', () => {
                checkbox.checked = !checkbox.checked;
                checkbox.dispatchEvent(new Event('change'));
            });

            this.displayElement.appendChild(urlSwitchContainer);
        }

        async displayJsonData(jsonData) {
            this.jsonData = jsonData;
            this.indexData(jsonData.subject);
            this.filteredData = jsonData.subject;
            this.renderTable();
        }

        indexData(data) {
            this.indexedData = {
                subjectCode: {},
                subjectName: {},
                semester: {},
                format: {},
                dayOfWeek: {},
                period: {},
            };

            data.forEach((subject, index) => {
                const subjectCode = subject[0];
                const subjectName = subject[1];
                const semester = subject[5];
                const format = subject[10];
                const timetable = subject[6];
                const dayOfWeek = timetable.slice(0, 1);
                const period = timetable.slice(1);

                if (!this.indexedData.subjectCode[subjectCode]) {
                    this.indexedData.subjectCode[subjectCode] = [];
                }
                this.indexedData.subjectCode[subjectCode].push(index);

                if (!this.indexedData.subjectName[subjectName]) {
                    this.indexedData.subjectName[subjectName] = [];
                }
                this.indexedData.subjectName[subjectName].push(index);

                if (!this.indexedData.semester[semester]) {
                    this.indexedData.semester[semester] = [];
                }
                this.indexedData.semester[semester].push(index);

                if (!this.indexedData.format[format]) {
                    this.indexedData.format[format] = [];
                }
                this.indexedData.format[format].push(index);

                if (!this.indexedData.dayOfWeek[dayOfWeek]) {
                    this.indexedData.dayOfWeek[dayOfWeek] = [];
                }
                this.indexedData.dayOfWeek[dayOfWeek].push(index);

                if (!this.indexedData.period[period]) {
                    this.indexedData.period[period] = [];
                }
                this.indexedData.period[period].push(index);
            });
        }

        renderTable() {
            const tableHeaders = ['  ', '科目番号', '科目名', '単位', '年次', '開講時期', '時間割', '教室', '担当教員', '概要', '備考'];

            let tableHtml = `
            <table style="border-collapse: collapse; width: 100%;">
                <thead>
                    <tr style="background-color: #f2f2f2;">
                        ${tableHeaders.map(header => `<th style="border: 1px solid #ddd; padding: 12px; text-align: left;">${header}</th>`).join('')}
                    </tr>
                </thead>
                <tbody>
        `;

            const startIndex = (this.currentPage - 1) * this.pageSize;
            const endIndex = startIndex + this.pageSize;
            const paginatedData = this.filteredData.slice(startIndex, endIndex);

            if (paginatedData.length === 0) {
                tableHtml += `
                <tr>
                    <td colspan="${tableHeaders.length}" style="text-align: center; padding: 8px;">検索結果がありません</td>
                </tr>
            `;
            } else {
                paginatedData.forEach((subject, index) => {
                    const rowStyle = index % 2 === 0 ? 'background-color: #f9f9f9;' : '';
                    tableHtml += `
                    <tr style="${rowStyle}">
                        <td style="border: 1px solid #ddd; padding: 8px;">
                        <button class="subject-button syllabus" data-subject-code="${subject[0]}" style="width: 80px;">シラバス</button>
                        <button class="subject-button add" data-subject-code="${subject[0]}" style="width: 80px;">科目を追加</button>
                        </td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[0]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[1]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[3]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[4]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[5]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[6]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[7]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[8]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[9]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[10]}</td>
                    </tr>
                `;
                });
            }

            tableHtml += `
                </tbody>
            </table>
        `;

            const totalPages = Math.ceil(this.filteredData.length / this.pageSize);
            const paginationHtml = `
            <div style="margin-top: 10px;">
                <button id="prevPage" style="margin-right: 5px;">前</button>
                <span>ページ ${this.currentPage} / ${totalPages}</span>
                <button id="nextPage" style="margin-left: 5px;">次</button>
            </div>
        `;

            this.tableContainer.innerHTML = tableHtml + paginationHtml;

            const prevPageButton = this.tableContainer.querySelector('#prevPage');
            const nextPageButton = this.tableContainer.querySelector('#nextPage');

            prevPageButton.addEventListener('click', () => {
                if (this.currentPage > 1) {
                    this.currentPage--;
                    this.renderTable();
                }
            });

            nextPageButton.addEventListener('click', () => {
                if (this.currentPage < totalPages) {
                    this.currentPage++;
                    this.renderTable();
                }
            });

            this.addSubjectButtonListeners();
        }

        addSubjectButtonListeners() {
            const addButtons = this.tableContainer.querySelectorAll('.subject-button.add');
            addButtons.forEach((button) => {
                button.addEventListener('click', () => {
                    const subjectCode = button.dataset.subjectCode;
                    const subjectName = button.closest('tr').querySelector('td:nth-child(3)').textContent.trim();

                    const confirmAdd = window.confirm(`科目名 ${subjectName} (科目番号 : ${subjectCode}) を追加しますか?`);
                    if (confirmAdd) {
                        this.handleSubjectButtonClick(subjectCode, subjectName, 'add');
                    }
                });
            });

            const syllabusButtons = this.tableContainer.querySelectorAll('.subject-button.syllabus');
            syllabusButtons.forEach((button) => {
                button.addEventListener('click', () => {
                    const subjectCode = button.dataset.subjectCode;
                    this.handleSubjectButtonClick(subjectCode, '', 'syllabus');
                });
            });
        }

        handleSubjectButtonClick(subjectCode, subjectName, action) {
            if (!this.keyObserver) {
                console.error('KeyObserver or its required properties are not initialized');
                return;
            }

            if (action === 'add') {
                try {
                    this.keyObserver.inputField.value = subjectCode;
                    this.keyObserver.addButton.click();
                } catch (error) {
                    console.error('Error adding subject:', error);
                    alert('科目の追加中にエラーが発生しました。');
                }
            } else if (action === 'syllabus') {
                const syllabusUrl = `https://kdb.tsukuba.ac.jp/syllabi/${new Date().getFullYear()}/${subjectCode}/jpn`;
                window.open(syllabusUrl, '_blank', 'width=800,height=600,resizable=yes,scrollbars=yes');
            }
        }

        filterData() {
            const subjectCodeInput = this.displayElement.querySelector('input[placeholder="科目番号で検索"]');
            const subjectNameInput = this.displayElement.querySelector('input[placeholder="科目名で検索"]');
            const semesterCheckboxes = this.displayElement.querySelectorAll('input[id^="semester-"]:checked');
            const onlineOfflineCheckboxes = this.displayElement.querySelectorAll('input[id^="format-"]:checked');

            const subjectCode = subjectCodeInput.value.toLowerCase();
            const subjectName = subjectNameInput.value.toLowerCase();
            const selectedDayOfWeek = this.dayOfWeekSelect.value;
            const selectedPeriod = this.periodSelect.value;
            const selectedSemesters = Array.from(semesterCheckboxes).map(checkbox => checkbox.value);
            const selectedFormats = Array.from(onlineOfflineCheckboxes).map(checkbox => checkbox.value);

            let filteredIndices = this.getIntersection(
                this.searchIndex(this.indexedData.subjectCode, subjectCode),
                this.searchIndex(this.indexedData.subjectName, subjectName),
                this.filterIndexByIncludingOptions(this.indexedData.dayOfWeek, [selectedDayOfWeek]),
                this.filterIndexByIncludingOptions(this.indexedData.period, [selectedPeriod]),
                this.filterIndexBySemesterModules(this.indexedData.semester, selectedSemesters),
                this.filterIndexByIncludingOptions(this.indexedData.format, selectedFormats)
            );

            this.filteredData = filteredIndices.map(index => this.jsonData.subject[index]);

            this.currentPage = 1;
            this.renderTable();
        }

        searchIndex(index, query) {
            if (query === '') {
                return Object.values(index).flat();
            }
            const matches = Object.entries(index).filter(([key]) => key.toLowerCase().includes(query));
            return matches.map(([_, indices]) => indices).flat();
        }

        /**
         * @param {Object} index - 検索対象のindex
         * @param {string[]} selectedOptions - 選択したoptionsの配列
         * @returns {number[]} - 選択したoptionsを含む項目のindex配列
         */

        filterIndexByIncludingOptions(index, selectedOptions) {
            if (selectedOptions.length === 0) {
                return Object.values(index).flat();
            }
            const matches = Object.entries(index).filter(([key, indices]) => selectedOptions.some(option => key.includes(option)));
            return matches.map(([_, indices]) => indices).flat();
        }

        filterIndexBySemesterModules(index, selectedSemesters) {
            if (selectedSemesters.length === 0) {
                return Object.values(index).flat();
            }

            const matches = Object.entries(index).filter(([key, indices]) => {
                return selectedSemesters.every(semester => key.includes(semester));
            });

            return matches.map(([_, indices]) => indices).flat();
        }

        getIntersection(...arrays) {
            return arrays.reduce((a, b) => a.filter(c => b.includes(c)));
        }

        sortData() {
            const sortSelect = this.displayElement.querySelector('select');
            const sortOrderSelect = sortSelect.nextElementSibling;
            const sortBy = sortSelect.value;
            const sortOrder = sortOrderSelect.value;

            const subjectIndex = sortBy === 'subjectCode' ? 0 : 1;

            this.filteredData.sort((a, b) => {
                const subjectA = a[subjectIndex].toLowerCase();
                const subjectB = b[subjectIndex].toLowerCase();

                if (subjectA < subjectB) return sortOrder === 'asc' ? -1 : 1;
                if (subjectA > subjectB) return sortOrder === 'asc' ? 1 : -1;
                return 0;
            });

            this.currentPage = 1;
            this.renderTable();
        }
    }

    class LogAnalyzer {
        constructor() {
            this.logKey = 'flowExecutionLogs';
        }

        getLogs() {
            const logs = sessionStorage.getItem(this.logKey);
            return logs ? JSON.parse(logs) : [];
        }

        sortLogsByTimestamp(logs) {
            return logs.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
        }

        cleanUpLogs() {
            const logs = this.getLogs();
            const maxLogs = 10;
            const minLogsToKeep = 5;

            if (logs.length > maxLogs) {
                const sortedLogs = this.sortLogsByTimestamp(logs);
                const logsToKeep = sortedLogs.slice(-minLogsToKeep);

                sessionStorage.setItem(this.logKey, JSON.stringify(logsToKeep));
            }
        }

        detectAbnormalPattern(logs) {
            const sortedLogs = this.sortLogsByTimestamp(logs);
            const validPattern = sortedLogs.slice(-3);

            const patternTypes = validPattern.map(log => log.type).join('-');
            return patternTypes === 'input-insert-back';
        }

        analyzeLogs() {
            this.cleanUpLogs();

            const logs = this.getLogs();
            const isAbnormal = this.detectAbnormalPattern(logs);

            if (isAbnormal) {
                alert("科目が追加されませんでした。時限・曜日を確認してください。");
            }
        }
    }

    // kdbっぽいなにか https://github.com/Make-IT-TSUKUBA/alternative-tsukuba-kdb から取ってきています。
    const jsonUrls = [
        'https://raw.githubusercontent.com/Make-IT-TSUKUBA/alternative-tsukuba-kdb/main/src/kdb.json', // 学類
        'https://raw.githubusercontent.com/Make-IT-TSUKUBA/alternative-tsukuba-kdb/main/src/kdb-grad.json' // 大学院
    ];
    new kdb_Displayer(jsonUrls);

    let keyObserver = null;
    let advancedSyllabus = null;

    function initializeEnhancer() {
        const targetIframe = document.querySelector('iframe[src*="campussquare.do?_flowId=RSW0001000-flow"]');
        if (targetIframe) {
            targetIframe.addEventListener('load', function () {
                try {
                    const iframeDocument = targetIframe.contentDocument || targetIframe.contentWindow.document;
                    if (iframeDocument.readyState === 'complete') {
                        keyObserver = new KeyObserver('body');
                        advancedSyllabus = new AdvancedSyllabus(keyObserver);

                        new kdb_Displayer(jsonUrls, keyObserver);
                    } else {
                        setTimeout(initializeEnhancer, 100);
                    }
                } catch (error) {
                    console.error('Error accessing iframe content:', error);
                    setTimeout(initializeEnhancer, 1000);
                }
            });
        } else {
            console.warn('Target iframe not found, retrying in 1 second');
            setTimeout(initializeEnhancer, 1000);
        }
    }

    function observePageChanges() {
        const observerOptions = {
            childList: true,
            subtree: true
        };

        const observer = new MutationObserver(function(mutationsList, observer) {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    const addedIframes = Array.from(mutation.addedNodes).filter(node => node.nodeType === Node.ELEMENT_NODE && node.tagName === 'IFRAME');
                    if (addedIframes.length > 0) {
                        console.log('New iframe(s) added:', addedIframes);
                        initializeEnhancer();
                    }
                }
            }
        });

        observer.observe(document.body, observerOptions);
    }
    observePageChanges();

})();