NetSchool Tweaks

Дополнительные инструменты для NetSchool / NetCity (Сетевой Город. Образование)

当前为 2024-03-09 提交的版本,查看 最新版本

// ==UserScript==
// @name         NetSchool Tweaks
// @namespace    https://greasyfork.org/users/843419
// @description  Дополнительные инструменты для NetSchool / NetCity (Сетевой Город. Образование)
// @version      1.0.4
// @author       Zgoly
// @match        *://*/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=ir-tech.ru
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

try {
    console.log(language.Generic.Calendar.kTitle1, 'найден, NetSchool Tweaks активен')
} catch {
    return
}

let autoLogin = GM_getValue('autoLogin', false)
let loginName = GM_getValue('loginName', 'Пользователь')
let password = GM_getValue('password', '12345678')
let schoolId = GM_getValue('schoolId', '')
let autoSkip = GM_getValue('autoSkip', true)

function waitForElement(selector) {
    return new Promise(resolve => {
        if (document.querySelector(selector)) return resolve(document.querySelector(selector))

        const observer = new MutationObserver(mutations => {
            if (document.querySelector(selector)) {
                observer.disconnect()
                resolve(document.querySelector(selector))
            }
        })

        observer.observe(document.body, { childList: true, subtree: true })
    })
}

if (autoLogin && window.location.pathname.startsWith('/authorize')) {
    function runAngularAction() {
        try {
            angular.element(document.body).scope().$$childTail.$ctrl.loginStrategiesService.loginWithLoginPassCheck(loginName, password, schoolId, null, { 'idpBindUser': 1 })
        } catch {
            requestAnimationFrame(runAngularAction)
        }
    }

    runAngularAction()
}

waitForElement('ns-modal').then((element) => {
    if (autoSkip && element.getAttribute('header') == language.Generic.Login.kTitleSecurityWarning) {
        element.querySelector('button').click()
        console.log('Security warning modal skipped')
    }
})

function nstSwitch(parentElement) {
    let label = document.createElement('label')
    label.classList.add('nst-switch')

    let input = document.createElement('input')
    input.type = 'checkbox'
    input.classList.add('nst-hide')

    let div = document.createElement('div')

    label.append(input)
    label.append(div)

    parentElement.append(label)

    return input
}

function nstModal(headlineText, contentHTML, showSaveButton = true) {
    return new Promise((resolve, reject) => {
        let dialog = document.createElement('dialog')
        dialog.classList.add('nst-dialog')

        let dialogWrapper = document.createElement('div')
        dialogWrapper.classList.add('nst-dialog-wrapper')
        dialog.append(dialogWrapper)

        let headline = document.createElement('p')
        headline.classList.add('nst-headline')
        headline.textContent = headlineText
        dialogWrapper.append(headline)

        let dialogAutofocus = document.createElement('input')
        dialogAutofocus.autofocus = 'autofocus'
        dialogAutofocus.style.display = 'none'
        dialogWrapper.append(dialogAutofocus)

        let content = document.createElement('div')
        content.classList.add('nst-content')
        content.append(contentHTML)
        dialogWrapper.append(content)

        let actions = document.createElement('div')
        actions.classList.add('nst-actions')

        let closeButton = document.createElement('button')
        closeButton.classList.add('nst-close')
        closeButton.textContent = 'Закрыть'
        closeButton.addEventListener('click', () => closeDialog(false))
        actions.append(closeButton)

        if (showSaveButton) {
            let saveButton = document.createElement('button')
            saveButton.classList.add('nst-save')
            saveButton.textContent = 'Сохранить'
            saveButton.addEventListener('click', () => closeDialog(true))
            actions.append(saveButton)
        }

        dialogWrapper.append(actions)

        document.body.append(dialog)

        dialog.showModal()
        // Убираем фокус с поля ввода
        document.activeElement.blur()

        document.body.classList.add('nst-no-scroll')

        function closeDialog(result = false) {
            dialog.classList.add('nst-hide-dialog')

            setTimeout(() => {
                dialog.remove()
                if (document.getElementsByTagName('dialog').length < 1) document.body.classList.remove('nst-no-scroll')
            }, 500)

            resolve(result)
        }

        contentHTML.closeDialog = closeDialog

        dialog.addEventListener('click', (event) => {
            if (event.target === dialog) {
                closeDialog(false)
            }
        })

        dialog.addEventListener('close', () => closeDialog())
        dialog.addEventListener('error', reject)
    })
}

let settings = document.createElement('li')
let settingsLink = document.createElement('a')
settings.append(settingsLink)

let settingsBody = document.createElement('span')
settingsBody.classList.add('cb-settings')
settingsLink.append(settingsBody)

let settingsIcon = document.createElement('i')
settingsIcon.classList.add('icon-gear', 'nst-settings-icon')
settingsBody.append(settingsIcon)

settingsBody.addEventListener('click', () => {
    let div = document.createElement('div')
    let table = document.createElement('table')

    // Переключатель авто входа
    let autoLoginRow = document.createElement('tr')

    let autoLoginLabelCell = document.createElement('td')

    let autoLoginLabelTitle = document.createElement('div')
    autoLoginLabelTitle.classList.add('nst-label-title')
    autoLoginLabelTitle.textContent = 'Авто вход'
    autoLoginLabelCell.append(autoLoginLabelTitle)

    let autoLoginLabelDescription = document.createElement('div')
    autoLoginLabelDescription.classList.add('nst-label-description')
    autoLoginLabelDescription.textContent = 'Авто вход по логину и паролю.'
    autoLoginLabelCell.append(autoLoginLabelDescription)

    let autoLoginInputCell = document.createElement('td')
    let autoLoginInput = nstSwitch(autoLoginInputCell)
    autoLoginInput.checked = autoLogin

    autoLoginRow.append(autoLoginLabelCell)
    autoLoginRow.append(autoLoginInputCell)

    table.append(autoLoginRow)

    // Поле логина
    let loginNameRow = document.createElement('tr')

    let loginNameLabelCell = document.createElement('td')

    let loginNameLabelTitle = document.createElement('div')
    loginNameLabelTitle.classList.add('nst-label-title')
    loginNameLabelTitle.textContent = 'Логин'
    loginNameLabelCell.append(loginNameLabelTitle)

    let loginNameLabelDescription = document.createElement('div')
    loginNameLabelDescription.classList.add('nst-label-description')
    loginNameLabelDescription.textContent = 'Логин для входа.'
    loginNameLabelCell.append(loginNameLabelDescription)

    let loginNameInputCell = document.createElement('td')
    loginNameInputCell.classList.add('nst-flex')
    let loginNameInput = document.createElement('input')
    loginNameInput.type = 'text'
    loginNameInput.value = loginName

    loginNameInputCell.append(loginNameInput)

    loginNameRow.append(loginNameLabelCell)
    loginNameRow.append(loginNameInputCell)

    table.append(loginNameRow)

    // Поле пароля
    let passwordRow = document.createElement('tr')

    let passwordLabelCell = document.createElement('td')

    let passwordLabelTitle = document.createElement('div')
    passwordLabelTitle.classList.add('nst-label-title')
    passwordLabelTitle.textContent = 'Пароль'
    passwordLabelCell.append(passwordLabelTitle)

    let passwordLabelDescription = document.createElement('div')
    passwordLabelDescription.classList.add('nst-label-description')
    passwordLabelDescription.textContent = 'Пароль для входа.'
    passwordLabelCell.append(passwordLabelDescription)

    let passwordInputCell = document.createElement('td')
    passwordInputCell.classList.add('nst-password-input-cell', 'nst-flex')
    let passwordInput = document.createElement('input')
    passwordInput.type = 'password'
    passwordInput.classList.add('nst-password')
    passwordInput.value = password

    let passwordEye = document.createElement('div')
    passwordEye.classList.add('nst-password-eye')
    passwordEye.addEventListener('click', () => {
        passwordInput.type = passwordInput.type === 'password' ? 'text' : 'password'
    })

    let passwordEyeInner = document.createElement('div')
    passwordEyeInner.classList.add('nst-password-eye-inner')
    let passwordEyeOuter = document.createElement('div')
    passwordEyeOuter.classList.add('nst-password-eye-outer')

    passwordEye.append(passwordEyeInner)
    passwordEye.append(passwordEyeOuter)

    passwordInputCell.append(passwordInput)
    passwordInputCell.append(passwordEye)

    passwordRow.append(passwordLabelCell)
    passwordRow.append(passwordInputCell)

    table.append(passwordRow)

    // Поле ID школы
    let schoolIdRow = document.createElement('tr')

    let schoolIdLabelCell = document.createElement('td')

    let schoolIdLabelTitle = document.createElement('div')
    schoolIdLabelTitle.classList.add('nst-label-title')
    schoolIdLabelTitle.textContent = 'ID школы'
    schoolIdLabelCell.append(schoolIdLabelTitle)

    let schoolIdLabelDescription = document.createElement('div')
    schoolIdLabelDescription.classList.add('nst-label-description')
    schoolIdLabelDescription.textContent = 'ID школы для входа. Оставьте пустым, если не знаете.'
    schoolIdLabelCell.append(schoolIdLabelDescription)

    let schoolIdInputCell = document.createElement('td')
    schoolIdInputCell.classList.add('nst-flex')
    let schoolIdInput = document.createElement('input')
    schoolIdInput.type = 'text'
    schoolIdInput.value = schoolId
    schoolIdInput.placeholder = schoolId

    schoolIdInputCell.append(schoolIdInput)

    schoolIdRow.append(schoolIdLabelCell)
    schoolIdRow.append(schoolIdInputCell)

    table.append(schoolIdRow)

    // Переключатель авто пропуска
    let autoSkipRow = document.createElement('tr')

    let autoSkipLabelCell = document.createElement('td')

    let autoSkipLabelTitle = document.createElement('div')
    autoSkipLabelTitle.classList.add('nst-label-title')
    autoSkipLabelTitle.textContent = 'Авто пропуск'
    autoSkipLabelCell.append(autoSkipLabelTitle)

    let autoSkipLabelDescription = document.createElement('div')
    autoSkipLabelDescription.classList.add('nst-label-description')
    autoSkipLabelDescription.textContent = 'Авто пропуск навязчивых уведомлений.'
    autoSkipLabelCell.append(autoSkipLabelDescription)

    let autoSkipInputCell = document.createElement('td')
    let autoSkipInput = nstSwitch(autoSkipInputCell)
    autoSkipInput.checked = autoSkip

    autoSkipRow.append(autoSkipLabelCell)
    autoSkipRow.append(autoSkipInputCell)

    table.append(autoSkipRow)

    function toggleFields() {
        let fields = [loginNameInput, passwordInput, schoolIdInput]
        fields.forEach(field => {
            field.disabled = !autoLoginInput.checked
        })
    }
    toggleFields()
    autoLoginInput.addEventListener('change', toggleFields)

    div.append(table)

    // Сохранение настроек
    nstModal('Настройки', div).then(save => {
        if (save) {
            GM_setValue('autoLogin', autoLoginInput.checked)
            autoLogin = autoLoginInput.checked
            GM_setValue('loginName', loginNameInput.value)
            loginName = loginNameInput.value
            GM_setValue('password', passwordInput.value)
            password = passwordInput.value
            GM_setValue('schoolId', schoolIdInput.value == '' ? appContext.schoolId : schoolIdInput.value)
            schoolId = schoolIdInput.value == '' ? appContext.schoolId : schoolIdInput.value
            GM_setValue('autoSkip', autoSkipInput.checked)
            autoSkip = autoSkipInput.checked
        }
    })

    let previewMarksWrapper = document.createElement('div')
    previewMarksWrapper.classList.add('preview-marks-wrapper')
    div.append(previewMarksWrapper)

    let at = appContext.at
    let weekStart = appContext.weekStart
    let weekEnd = appContext.weekEnd

    // Начало учебного года
    let startDateInput = document.createElement('input')
    startDateInput.type = 'date'
    startDateInput.value = weekStart
    previewMarksWrapper.append(startDateInput)

    // Конец учебного года
    let endDateInput = document.createElement('input')
    endDateInput.type = 'date'
    endDateInput.value = weekEnd
    previewMarksWrapper.append(endDateInput)

    if (weekStart == undefined || weekEnd == undefined) {
        fetch('/webapi/v2/reports/studenttotal', { 'headers': { 'at': at } }).then((response) => {
            return response.json()
        }).then((data) => {
            weekStart = data.filterSources[3].defaultRange.start.substring(0, 10)
            startDateInput.value = weekStart
            weekEnd = data.filterSources[3].defaultRange.end.substring(0, 10)
            endDateInput.value = weekEnd
        })
    }

    // Кнопка предпросмотра оценок
    let previewMarksButton = document.createElement('button')
    previewMarksButton.innerText = 'Предпросмотр оценок'
    previewMarksButton.addEventListener('click', () => {
        let marksTableWrapper = document.createElement('div')
        marksTableWrapper.classList.add('nst-marks-table-wrapper')

        let contentDiv = document.createElement('div')
        contentDiv.classList.add('nst-content')
        contentDiv.append(marksTableWrapper)

        fetch('/webapi/student/diary/init', { 'headers': { 'at': at } }).then((response) => {
            return response.json()
        }).then((data) => {
            let studentId = data.students[0].studentId
            let yearId = appContext.yearId
            let startDate = startDateInput.value
            let endDate = endDateInput.value
            // Запрос дневика
            fetch(`/webapi/student/diary?studentId=${studentId}&weekStart=${startDate}&weekEnd=${endDate}&withLaAssigns=true&yearId=${yearId}`, { 'headers': { 'at': at } }).then((response) => {
                return response.json()
            }).then((data) => {
                // Повторный запрос дневника (с текущей датой для отображения правильной недели, игнорируется)
                let currentDate = date2strf(new Date(), 'yyyy\x01mm\x01dd\x01.')
                fetch(`/webapi/student/diary?studentId=${studentId}&weekStart=${currentDate}&weekEnd=${currentDate}&withLaAssigns=true&yearId=${yearId}`, { 'headers': { 'at': at } })

                let marksTable = document.createElement('table')
                marksTableWrapper.append(marksTable)

                let tableControlsDiv = document.createElement('div')
                tableControlsDiv.classList.add('nst-controls')
                contentDiv.append(tableControlsDiv)

                let selectAllButton = document.createElement('button')
                selectAllButton.innerText = 'Выбрать все'
                selectAllButton.addEventListener('click', () => {
                    for (let row of marksTable.rows) {
                        row.classList.add('nst-row-selected')
                    }
                    updateButtons()
                })
                tableControlsDiv.append(selectAllButton)

                let deselectAllButton = document.createElement('button')
                deselectAllButton.innerText = 'Отменить выбор'
                deselectAllButton.addEventListener('click', () => {
                    for (let row of marksTable.rows) {
                        row.classList.remove('nst-row-selected')
                    }
                    updateButtons()
                })
                tableControlsDiv.append(deselectAllButton)

                let addRowButton = document.createElement('button')
                addRowButton.innerText = 'Cоздать'
                addRowButton.addEventListener('click', () => {
                    let selectedRows = marksTable.querySelectorAll('.nst-row-selected')
                    let newRow = createRow()
                    if (selectedRows.length > 0) {
                        selectedRows[selectedRows.length - 1].after(newRow)
                        selectedRows.forEach(row => row.classList.remove('nst-row-selected'))
                    } else {
                        marksTable.prepend(newRow)
                    }
                    cookRow(newRow)

                    newRow.classList.add('nst-row-selected')
                    newRow.scrollIntoView({behavior: "smooth"})
                    updateButtons()
                })
                tableControlsDiv.append(addRowButton)

                let cloneRowButton = document.createElement('button')
                cloneRowButton.innerText = 'Клонировать'
                cloneRowButton.addEventListener('click', () => {
                    let selectedRows = marksTable.querySelectorAll('.nst-row-selected')
                    selectedRows.forEach(row => {
                        let clonedRow = row.cloneNode(true)
                        row.after(clonedRow)
                        cookRow(clonedRow)

                        let marksCell = clonedRow.querySelector('.nst-marks-cell')
                        Array.from(marksCell.children).forEach(markDiv => {
                            cookMark(markDiv)
                        })

                        row.classList.remove('nst-row-selected')
                    })
                    updateButtons()
                })
                tableControlsDiv.append(cloneRowButton)

                let removeRowButton = document.createElement('button')
                removeRowButton.innerText = 'Удалить'
                removeRowButton.addEventListener('click', () => {
                    let selectedRows = marksTable.querySelectorAll('.nst-row-selected')
                    selectedRows.forEach(row => row.remove())
                    updateButtons()
                })
                tableControlsDiv.append(removeRowButton)

                let addMarkButton = document.createElement('button')
                addMarkButton.innerText = 'Добавить оценку'
                addMarkButton.addEventListener('click', () => {
                    let selectedRows = marksTable.querySelectorAll('.nst-row-selected')
                    if (selectedRows.length == 0) return

                    let [templateTable, markInput, weightInput] = markModalTemplate(5, 20)

                    nstModal('Добавление оценки', templateTable).then(save => {
                        if (save) {
                            selectedRows.forEach(row => {
                                let mark = createMark(markInput.value, weightInput.value)
                                row.querySelector('.nst-marks-cell').append(mark)
                                cookMark(mark)
                                highlightMark(mark)
                                row.classList.remove('nst-row-selected')
                            })

                            updateButtons()
                        }
                    })
                })
                tableControlsDiv.append(addMarkButton)

                function createMark(mark, weight, fullAssignment = null) {
                    let markDiv = document.createElement('div')
                    if (fullAssignment) markDiv.dataset.assignment = JSON.stringify(fullAssignment)
                    markDiv.classList.add('nst-mark')

                    let markValue = document.createElement('p')
                    markValue.innerText = mark
                    markValue.classList.add('nst-mark-value')
                    markDiv.append(markValue)

                    let weightValue = document.createElement('p')
                    weightValue.innerText = weight
                    weightValue.classList.add('nst-weight-value')
                    markDiv.append(weightValue)

                    return markDiv
                }

                function createRow(name = '') {
                    let row = document.createElement('tr')

                    let nameCell = document.createElement('td')
                    let nameInput = document.createElement('input')
                    nameInput.value = name
                    nameInput.classList.add('nst-name-input')
                    nameInput.placeholder = "Имя предмета"
                    nameCell.append(nameInput)
                    row.append(nameCell)

                    let marksCell = document.createElement('td')
                    marksCell.classList.add('nst-marks-cell')
                    row.append(marksCell)

                    let totalCell = document.createElement('td')
                    totalCell.classList.add('nst-total-cell')
                    row.append(totalCell)

                    return row
                }

                function markModalTemplate(mark, weight) {
                    let templateTable = document.createElement('table')

                    let markRow = document.createElement('tr')
                    templateTable.append(markRow)

                    let markLabelCell = document.createElement('td')
                    markRow.append(markLabelCell)

                    let markLabelTitle = document.createElement('div')
                    markLabelTitle.classList.add('nst-label-title')
                    markLabelTitle.textContent = 'Оценка'
                    markLabelCell.append(markLabelTitle)

                    let markLabelDescription = document.createElement('div')
                    markLabelDescription.classList.add('nst-label-description')
                    markLabelDescription.textContent = 'Оценка.'
                    markLabelCell.append(markLabelDescription)

                    let markInputCell = document.createElement('td')
                    markInputCell.classList.add('nst-flex')
                    markRow.append(markInputCell)

                    let markInput = document.createElement('input')
                    markInput.readOnly = true
                    markInput.value = mark
                    markInputCell.append(markInput)

                    let markSelectorsDiv = document.createElement('div')
                    markSelectorsDiv.classList.add('nst-mark-selectors')
                    markInputCell.append(markSelectorsDiv)

                    let marks = ['5', '4', '3', '2', '•']

                    marks.forEach(mark => {
                        let markButton = document.createElement('button')

                        markButton.innerText = mark
                        markButton.addEventListener('click', () => markInput.value = mark)
                        markSelectorsDiv.append(markButton)
                    })

                    let weightRow = document.createElement('tr')
                    templateTable.append(weightRow)

                    let weightLabelCell = document.createElement('td')
                    weightRow.append(weightLabelCell)

                    let weightLabelTitle = document.createElement('div')
                    weightLabelTitle.classList.add('nst-label-title')
                    weightLabelTitle.textContent = 'Вес'
                    weightLabelCell.append(weightLabelTitle)

                    let weightLabelDescription = document.createElement('div')
                    weightLabelDescription.classList.add('nst-label-description')
                    weightLabelDescription.textContent = 'Вес оценки.'
                    weightLabelCell.append(weightLabelDescription)

                    let weightInputCell = document.createElement('td')
                    weightInputCell.classList.add('nst-flex')
                    weightRow.append(weightInputCell)

                    let weightInput = document.createElement('input')
                    weightInput.type = 'number'
                    weightInput.value = weight
                    weightInputCell.append(weightInput)

                    return [templateTable, markInput, weightInput]
                }

                function highlightMark(mark) {
                    mark.classList.remove('nst-mark-highlight');
                    mark.offsetWidth;
                    mark.classList.add('nst-mark-highlight');
                }

                function cookMark(mark) {
                    let markValue = mark.querySelector('.nst-mark-value')
                    let weightValue = mark.querySelector('.nst-weight-value')

                    mark.addEventListener('click', () => {
                        let modalDiv = document.createElement('div')
                        let [templateTable, markInput, weightInput] = markModalTemplate(markValue.innerText, weightValue.innerText)
                        modalDiv.append(templateTable)

                        let controlsDiv = document.createElement('div')
                        controlsDiv.classList.add('nst-controls')
                        modalDiv.append(controlsDiv)

                        let cloneMarkButton = document.createElement('button')
                        cloneMarkButton.innerText = 'Клонировать'
                        controlsDiv.append(cloneMarkButton)
                        cloneMarkButton.addEventListener('click', () => {
                            modalDiv.closeDialog(true)
                            let newMark = mark.cloneNode(true)
                            mark.after(newMark)
                            cookMark(newMark)
                            highlightMark(newMark)
                        })

                        let deleteMarkButton = document.createElement('button')
                        deleteMarkButton.innerText = 'Удалить'
                        controlsDiv.append(deleteMarkButton)
                        deleteMarkButton.addEventListener('click', () => {
                            mark.remove()
                            modalDiv.closeDialog(false)
                        })

                        if (mark.dataset.assignment) {
                            let assignment = JSON.parse(mark.dataset.assignment)

                            let restoreMarkButton = document.createElement('button')
                            restoreMarkButton.innerText = 'Восстановить'
                            controlsDiv.append(restoreMarkButton)
                            restoreMarkButton.addEventListener('click', () => {
                                markInput.value = assignment.mark
                                weightInput.value = assignment.weight
                            })

                            let assignmentMarkButton = document.createElement('button')
                            assignmentMarkButton.innerText = 'Подробности'
                            controlsDiv.append(assignmentMarkButton)
                            assignmentMarkButton.addEventListener('click', () => {
                                // TODO Доделать
                                let assignmentTable = document.createElement('table')

                                let translations = {
                                    'id': 'ID задания',
                                    'assignmentName': 'Тема задания',
                                    'activityName': 'Имя деятельности',
                                    'problemName': 'Название задачи',
                                    'studentId': 'ID ученика',
                                    'subjectGroup.id': 'ID предмета',
                                    'subjectGroup.name': 'Название предмета',
                                    'teachers.0.id': 'ID учителя',
                                    'teachers.0.name': 'Имя учителя',
                                    'productId': 'ID продукта',
                                    'isDeleted': 'Удалено',
                                    'weight': 'Вес',
                                    'date': 'Дата',
                                    'description': 'Описание',
                                    'mark': 'Оценка',
                                    'typeId': 'ID типа задания',
                                    'type': 'Тип задания'
                                }

                                for (let key in assignment) {
                                    let translation = translations[key] || key
                                    let value = assignment[key]
                                    value = value === true ? "Да" : value === false ? "Нет" : value

                                    let assignmentRow = document.createElement('tr')
                                    assignmentTable.append(assignmentRow)

                                    let assignmentLabelCell = document.createElement('td')
                                    assignmentLabelCell.innerText = translation
                                    assignmentRow.append(assignmentLabelCell)

                                    let assignmentInputCell = document.createElement('td')
                                    assignmentInputCell.classList.add('nst-flex')
                                    assignmentRow.append(assignmentInputCell)


                                    let assignmentInput

                                    console.log(key, value, typeof value)
                                    if (key === 'date') {
                                        assignmentInput = document.createElement('input')
                                        assignmentInput.readOnly = true
                                        assignmentInput.type = 'date'
                                        assignmentInput.value = value
                                    } else if (typeof value === 'number') {
                                        assignmentInput = document.createElement('input')
                                        assignmentInput.readOnly = true
                                        assignmentInput.type = 'number'
                                        assignmentInput.value = value
                                    } else {
                                        assignmentInput = document.createElement('div')
                                        assignmentInput.innerText = value
                                        assignmentInput.classList.add('nst-area')
                                    }

                                    assignmentInputCell.append(assignmentInput)
                                }

                                nstModal('Подробности задания', assignmentTable, false)
                            })
                        }

                        nstModal('Редактирование оценки', modalDiv).then(save => {
                            if (save) {
                                markValue.innerText = markInput.value
                                weightValue.innerText = weightInput.value
                                highlightMark(mark)
                            }
                        })
                    })
                }
                function updateButtons() {
                    if (marksTable.querySelectorAll('.nst-row-selected').length > 0) {
                        addMarkButton.disabled = false
                        cloneRowButton.disabled = false
                        removeRowButton.disabled = false
                    } else {
                        addMarkButton.disabled = true
                        cloneRowButton.disabled = true
                        removeRowButton.disabled = true
                    }
                }

                updateButtons()

                function cookRow(row) {
                    let marksCell = row.querySelector('.nst-marks-cell')
                    let totalCell = row.querySelector('.nst-total-cell')

                    // Изменение цвета оценки / балла
                    function calculateTotalScore() {
                        let markSum = 0
                        let weightSum = 0

                        Array.from(marksCell.children).forEach(markDiv => {
                            let markValue = markDiv.querySelector('.nst-mark-value')
                            let weightValue = markDiv.querySelector('.nst-weight-value')
                            let mark = markValue.innerText.replaceAll('•', '2')
                            let weight = Number(weightValue.innerText)

                            markValue.classList.remove(...['nst-mark-excellent', 'nst-mark-good', 'nst-mark-average', 'nst-mark-bad'])
                            markValue.classList.add(getMarkClass(mark))

                            markSum += mark * weight
                            weightSum += weight
                        })

                        totalCell.innerText = weightSum ? Number((markSum / weightSum).toFixed(2)) : 0
                        totalCell.classList.remove(...['nst-mark-excellent', 'nst-mark-good', 'nst-mark-average', 'nst-mark-bad'])
                        totalCell.classList.add(getMarkClass(totalCell.innerText))
                    }

                    function getMarkClass(mark) {
                        return Number(mark) >= 4.60 ? 'nst-mark-excellent' : Number(mark) >= 3.60 ? 'nst-mark-good' : Number(mark) >= 2.60 ? 'nst-mark-average' : 'nst-mark-bad'
                    }

                    let observer = new MutationObserver(() => {
                        calculateTotalScore()
                    })
                    observer.observe(marksCell, { childList: true, subtree: true })

                    calculateTotalScore()

                    // Выделение строки при нажатии на балл
                    totalCell.addEventListener('click', () => {
                        row.classList.toggle('nst-row-selected')
                        updateButtons()
                    })
                }

                function flattenJson(json) {
                    let result = {}

                    function flatten(obj, prefix = '') {
                        for (let key in obj) {
                            if (typeof obj[key] === 'object' && obj[key] !== null) {
                                flatten(obj[key], prefix + key + '.')
                            } else {
                                result[prefix + key] = obj[key]
                            }
                        }
                    }

                    flatten(json)
                    return result
                }

                fetch('/webapi/grade/assignment/types').then((response) => {
                    return response.json()
                }).then((types) => {
                    for (let day of data.weekDays) {
                        for (let lesson of day.lessons) {
                            if (Array.isArray(lesson.assignments)) {
                                for (let assignment of lesson.assignments) {
                                    if (assignment.mark) {
                                        fetch(`/webapi/student/diary/assigns/${assignment.id}`, { 'headers': { 'at': at } }).then((response) => {
                                            return response.json()
                                        }).then((fullAssignment) => {
                                            // Модификация данных в удобный формат
                                            fullAssignment.mark = assignment.mark.mark
                                            fullAssignment.studentId = assignment.mark.studentId
                                            fullAssignment.typeId = assignment.typeId
                                            fullAssignment.date = fullAssignment.date.substring(0, 10)

                                            let item = types.find(data => data.id == fullAssignment.typeId)
                                            fullAssignment.type = item.name

                                            fullAssignment = flattenJson(fullAssignment)

                                            for (let key in fullAssignment) {
                                                if (fullAssignment[key] == null) delete fullAssignment[key]
                                            }

                                            // Объявление / создание ряда
                                            let row = Array.from(marksTable.rows).find(r => r.querySelector('.nst-name-input').value == lesson.subjectName)
                                            if (!row) {
                                                row = createRow(lesson.subjectName)
                                                marksTable.append(row)
                                                cookRow(row)
                                            }

                                            // Добавление оценки
                                            let createdMark = createMark(fullAssignment.mark, fullAssignment.weight, fullAssignment)
                                            row.querySelector('.nst-marks-cell').append(createdMark)
                                            cookMark(createdMark)
                                        })
                                    }
                                }
                            }
                        }
                    }
                }).catch(err => {
                    console.error('Произошла ошибка: ', err)
                })
            })
        })

        nstModal('Предпросмотр оценок', contentDiv, false)
    })
    previewMarksWrapper.append(previewMarksButton)
})

waitForElement('.top-right-menu').then((elm) => {
    elm.prepend(settings)
})

GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=Nunito&display=swap');

:root {
    --nst-primary: #64a0c8;
    --nst-secondary: #415f78;
    --nst-tertiary: #374b5f;
    --nst-quaternary: #232a32;
    --nst-quinary: #1a1c1e;
    --nst-senary: #141618;

    --nst-text-primary: #c8e6ff;
    --nst-text-secondary: #aadcff;
    --nst-text-tertiary: #87afc8;
}

.nst-no-scroll {
    touch-action: none;
    overflow: hidden;
}

.nst-flex {
    display: flex;
    flex-direction: column;
}

.nst-flex input {
    flex: 1;
}

.nst-settings-icon {
    display: flex !important;
    justify-content: center;
    color: white;
    scale: 0.75;
}

.nst-dialog {
    border: none;
    outline: none;
    background: var(--nst-quinary);
    border-radius: 32px;
    box-shadow: rgba(0, 0, 0, 0.25) 0 0 25px;
    padding: 0;
}

.nst-dialog-wrapper {
    display: flex;
    flex-direction: column;
    padding: 24px;
    max-height: calc(100vh - 48px);
    max-width: calc(100vw - 48px);
}

.nst-content {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: auto;
}

.nst-dialog .preview-marks-wrapper {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    padding-top: 16px;
    gap: 8px;
}

.nst-dialog * {
    font-family: 'Nunito';
    color: var(--nst-text-secondary);
}

.nst-dialog .nst-headline:first-child {
    margin-top: 0px;
}

.nst-dialog .nst-headline {
    color: var(--nst-text-primary);
    font-size: 1.5em;
    margin: 0px;
    margin-bottom: 16px;
}

.nst-dialog .nst-actions {
    margin-top: 16px;
    display: flex;
    gap: 8px;
    justify-content: flex-end;
}

.nst-dialog button {
    cursor: pointer;
    color: var(--button-color);
    border-radius: 28px;
    padding: 12px;
    margin: 0;
    border: none;
    outline: none;
    transition: 0.2s;
    background: var(--nst-quaternary);
}

.nst-dialog button:not([disabled]):hover {
    background: var(--nst-tertiary);
}

.nst-dialog button:not([disabled]):active {
    background: var(--nst-secondary);
}

.nst-dialog button[disabled] {
    opacity: 0.5;
    cursor: default;
}

.nst-dialog input {
    text-shadow: none;
    box-shadow: none;
    line-height: normal;
    border: 2px solid var(--nst-tertiary);
    color: var(--nst-text-secondary);
    border-radius: 16px;
    padding: 12px;
    background: var(--nst-quaternary);
    transition: 0.2s;
}

.nst-dialog input::-webkit-outer-spin-button,
.nst-dialog input::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

.nst-dialog input[disabled] {
    border: 2px solid var(--nst-tertiary);
    opacity: 0.5;
}

.nst-dialog input[disabled]:hover,
.nst-dialog input[disabled]:focus,
.nst-dialog input[disabled]:active {
    border: 2px solid transparent;
    border-radius: 16px;
    color: var(--nst-text-secondary);
    box-shadow: none;
    padding: 12px;
}

.nst-dialog input:hover {
    border: 2px solid transparent;
    color: var(--nst-text-secondary);
    box-shadow: none;
}

.nst-dialog input:focus,
.nst-dialog input:active {
    border: 2px solid transparent;
    color: var(--nst-text-secondary);
    background: var(--nst-tertiary);
    box-shadow: none;
}

.nst-password {
    padding-right: 48px;
}

.nst-password ~ .nst-password-eye {
    transition: 0.2s;
    position: absolute;
    display: flex;
    justify-content: center;
    align-items: center;
    right: 8px;
    top: 8px;
    width: 48px;
    height: 48px;
}

.nst-password ~ .nst-password-eye:hover {
    opacity: 0.75;
}

.nst-password ~ .nst-password-eye:active {
    opacity: 0.5;
}

.nst-password ~ .nst-password-eye::after {
    transition: 0.2s;
    content: "";
    position: absolute;
    width: 60%;
    height: 2px;
    background-color: currentColor;
    transform: rotate(45deg);
    border-radius: 2px;
}

.nst-password[type="password"] ~ .nst-password-eye::after {
    width: 0%;
}

.nst-password ~ .nst-password-eye .nst-password-eye-inner {
    width: 24px;
    height: 16px;
    border: 2px solid currentColor;
    border-radius: 50%;
    position: absolute;
}

.nst-password ~ .nst-password-eye .nst-password-eye-outer {
    width: 8px;
    height: 8px;
    border: 2px solid currentColor;
    border-radius: 50%;
    position: absolute;
}

.nst-dialog .nst-area {
    border-radius: 16px;
    background: var(--nst-quaternary);
    padding: 16px;
    width: 100%;
    box-sizing: border-box;
}

.nst-dialog .nst-switch {
    position: relative;
    display: inline-block;
    width: 3.5em;
    height: 2em;
    margin: 0;
}

.nst-dialog .nst-switch .nst-hide {
    opacity: 0;
    width: 0;
    height: 0;
}

.nst-dialog .nst-switch div {
    position: absolute;
    cursor: pointer;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: var(--nst-quaternary);
    border: 2px solid var(--nst-tertiary);
    border-radius: 24px;
    transition: .4s;
}

.nst-dialog .nst-switch div:before {
    position: absolute;
    content: "";
    height: 1.2em;
    width: 1.2em;
    left: calc(0.4em - 2px);
    top: calc(0.4em - 2px);
    background: var(--nst-tertiary);
    border-radius: 50%;
    transition: .4s;
}

.nst-dialog .nst-switch input:checked + div {
    background: var(--nst-primary);
    border: 2px solid transparent;
}

.nst-dialog .nst-switch input:checked + div:before {
    transform: translateX(1.4em);
    background: rgba(0, 0, 0, 0.5);
}

.nst-dialog table {
    margin-left: auto;
    margin-right: auto;
}

.nst-dialog td {
    padding: 8px;
    position: relative;
}

.nst-total-cell {
    right: 0;
    position: sticky !important;
    cursor: pointer;
    background: var(--nst-quinary);
    transition: 0.1s;
}

tr.nst-row-selected .nst-total-cell:last-child {
    background: var(--nst-senary);
}

.nst-dialog .nst-marks-table-wrapper tr {
    transition: 0.2s;
}

.nst-dialog .nst-marks-table-wrapper tr.nst-row-selected {
    background: var(--nst-senary);
}

.nst-dialog .nst-marks-table-wrapper {
    overflow-y: auto;
    border-radius: 24px;
}

.nst-marks-cell {
    display: flex;
}

.nst-controls {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-evenly;
    gap: 8px;
    padding-top: 8px;
}

.nst-controls button {
    flex-grow: 1;
}

.nst-dialog[open] {
    animation: nst-show-dialog 0.5s forwards;
}

.nst-dialog.nst-hide-dialog {
    animation: nst-hide-dialog 0.5s forwards;
}

@keyframes nst-show-dialog {
    from {
        opacity: 0;
        transform: scale(0.5);
    }

    to {
        opacity: 1;
        transform: scale(1);
    }
}

@keyframes nst-hide-dialog {
    to {
        opacity: 0;
        transform: scale(0.5);
    }
}

.nst-dialog::backdrop {
    background: rgba(0, 0, 0, 0.5);
    backdrop-filter: blur(5px);
    animation: none;
}

.nst-dialog[open]::backdrop {
    animation: nst-show-opacity 0.5s forwards;
}

.nst-dialog.nst-hide-dialog::backdrop {
    animation: nst-hide-opacity 0.5s forwards;
}

@keyframes nst-show-opacity {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

@keyframes nst-hide-opacity {
    to {
        opacity: 0;
    }
}

.nst-label-title {
    font-size: 18px;
}

.nst-label-description {
    font-size: 12px;
    color: var(--nst-text-tertiary);
}

.nst-mark {
    min-width: 24px;
    display: flex;
    flex-flow: column wrap;
    align-items: stretch;
    cursor: pointer;
    animation: nst-show-opacity 0.5s forwards;
}

.nst-mark-selectors {
    display: flex;
    gap: 8px;
    padding-top: 8px;
    justify-content: space-between;
}

.nst-mark p {
    text-align: center;
    margin: 0px;
}

.nst-mark p:first-child {
    font-size: large;
}

.nst-mark p:nth-child(2) {
    color: gray;
    font-size: x-small;
}

.nst-mark-highlight {
    animation: nst-mark-highlight 3s forwards;
}

@keyframes nst-mark-highlight {
    0% {
        opacity: 1;
    } 16% {
        opacity: 0;
    } 32% {
        opacity: 1;
    } 48% {
        opacity: 0;
    } 64% {
        opacity: 1;
    } 80% {
        opacity: 0;
    } 96% {
        opacity: 1;
    }
}

.nst-mark-excellent {
    color: #96e400;
}

.nst-mark-good {
    color: #00c8ff;
}

.nst-mark-average {
    color: #f09600;
}

.nst-mark-bad {
    color: #ff3232;
}
`)