Дополнительные инструменты для NetSchool / NetCity (Сетевой Город. Образование)
// ==UserScript==// @name NetSchool Tweaks// @namespace https://gfork.dahi.icu/users/843419// @description Дополнительные инструменты для NetSchool / NetCity (Сетевой Город. Образование)// @version 1.0.5// @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', 0)let autoSkip = GM_getValue('autoSkip', true)let rowsSortMode = GM_getValue('rowsSortMode', 0)let marksSortMode = GM_getValue('marksSortMode', 0)let dot = '•'let dotMark = 2function waitForElement(selector) {return new Promise(resolve => {if (document.querySelector(selector)) return resolve(document.querySelector(selector))let 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('Модальное окно предупреждения о безопасности пропущено')}})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 = headlineTextdialogWrapper.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(r###lt = 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(r###lt)}contentHTML.closeDialog = closeDialogdialog.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 = autoLoginautoLoginRow.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')let loginNameInput = document.createElement('input')loginNameInput.type = 'text'loginNameInput.value = loginNameloginNameInputCell.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')let passwordInput = document.createElement('input')passwordInput.type = 'password'passwordInput.value = passwordpasswordInput.addEventListener('focus', () => passwordInput.type = 'text')passwordInput.addEventListener('blur', () => passwordInput.type = 'password')passwordInputCell.append(passwordInput)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')let schoolIdInput = document.createElement('input')schoolIdInput.type = 'number'schoolIdInput.value = schoolIdschoolIdInput.placeholder = schoolIdschoolIdInputCell.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 = autoSkipautoSkipRow.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.checkedGM_setValue('loginName', loginNameInput.value)loginName = loginNameInput.valueGM_setValue('password', passwordInput.value)password = passwordInput.valueGM_setValue('schoolId', schoolIdInput.value == '' ? appContext.schoolId : schoolIdInput.value)schoolId = schoolIdInput.value == '' ? appContext.schoolId : schoolIdInput.valueGM_setValue('autoSkip', autoSkipInput.checked)autoSkip = autoSkipInput.checked}})let previewMarksWrapper = document.createElement('div')previewMarksWrapper.classList.add('preview-marks-wrapper')div.append(previewMarksWrapper)let at = appContext.atlet weekStart = appContext.weekStartlet weekEnd = appContext.weekEnd// Начало учебного годаlet startDateInput = document.createElement('input')startDateInput.type = 'date'startDateInput.value = weekStartpreviewMarksWrapper.append(startDateInput)// Конец учебного годаlet endDateInput = document.createElement('input')endDateInput.type = 'date'endDateInput.value = weekEndpreviewMarksWrapper.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 = weekStartweekEnd = 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].studentIdlet yearId = appContext.yearIdlet startDate = startDateInput.valuelet 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 sortButton = document.createElement('button')sortButton.innerText = 'Сортировать'sortButton.addEventListener('click', () => {let sortDiv = document.createElement('div')let sortRowsTitle = document.createElement('p')sortRowsTitle.classList.add('nst-label-title')sortRowsTitle.innerText = 'Сортировка строк'sortDiv.append(sortRowsTitle)let sortRowsOptions = ['Не сортировать','По имени (по возрастанию)','По имени (по убыванию)','По количеству оценок (по возрастанию)','По количеству оценок (по убыванию)','По дате (по возрастанию)','По дате (по убыванию)','По среднему баллу (по возрастанию)','По среднему баллу (по убыванию)']let selectedSortRowsOption = rowsSortModesortRowsOptions.forEach((option, index) => {let sortRowsDiv = document.createElement('div')sortRowsDiv.classList.add('nst-radio-wrapper')sortDiv.append(sortRowsDiv)let sortRowsOption = document.createElement('input')sortRowsOption.type = 'radio'sortRowsOption.name = 'sortRows'sortRowsOption.value = indexsortRowsOption.id = `sortRows${index}`if (index == rowsSortMode) sortRowsOption.checked = truesortRowsOption.addEventListener('click', () => selectedSortRowsOption = Number(sortRowsOption.value))sortRowsDiv.append(sortRowsOption)let sortRowsOptionLabel = document.createElement('label')sortRowsOptionLabel.htmlFor = `sortRows${index}`sortRowsOptionLabel.innerText = optionsortRowsDiv.append(sortRowsOptionLabel)})let sortMarksTitle = document.createElement('p')sortMarksTitle.classList.add('nst-label-title')sortMarksTitle.innerText = 'Сортировка оценок'sortDiv.append(sortMarksTitle)let sortMarksOptions = ['Не сортировать','По дате (по возрастанию)','По дате (по убыванию)','По оценке (по возрастанию)','По оценке (по убыванию)','По весу (по возрастанию)','По весу (по убыванию)']let selectedSortMarksOption = marksSortModesortMarksOptions.forEach((option, index) => {let sortMarksDiv = document.createElement('div')sortMarksDiv.classList.add('nst-radio-wrapper')sortDiv.append(sortMarksDiv)let sortMarksOption = document.createElement('input')sortMarksOption.type = 'radio'sortMarksOption.name = 'sortMarks'sortMarksOption.value = indexsortMarksOption.id = `sortMarks${index}`if (index == marksSortMode) sortMarksOption.checked = truesortMarksOption.addEventListener('click', () => selectedSortMarksOption = Number(sortMarksOption.value))sortMarksDiv.append(sortMarksOption)let sortMarksOptionLabel = document.createElement('label')sortMarksOptionLabel.htmlFor = `sortMarks${index}`sortMarksOptionLabel.innerText = optionsortMarksDiv.append(sortMarksOptionLabel)})nstModal('Сортировка', sortDiv).then(save => {if (save) {rowsSortMode = selectedSortRowsOptionGM_setValue('rowsSortMode', rowsSortMode)marksSortMode = selectedSortMarksOptionGM_setValue('marksSortMode', marksSortMode)sortTable()}})})tableControlsDiv.append(sortButton)// Кнопка для выбора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) returnlet [templateTable, markInput, weightInput] = markModalTemplate(5, 20)nstModal('Добавление оценки', templateTable).then(save => {if (save) {selectedRows.forEach(row => {let mark = createMark(markInput.value, weightInput.value)mark.dataset.assignment = JSON.stringify({ "date": new Date().toLocaleDateString("en-CA") })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 = markmarkValue.classList.add('nst-mark-value')markDiv.append(markValue)let weightValue = document.createElement('p')weightValue.innerText = weightweightValue.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 = namenameInput.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 markInputCell = document.createElement('td')markInputCell.classList.add('nst-flex')markRow.append(markInputCell)let markInput = document.createElement('input')markInput.readOnly = truemarkInput.value = markmarkInputCell.append(markInput)let markSelectorsDiv = document.createElement('div')markSelectorsDiv.classList.add('nst-mark-selectors')markInputCell.append(markSelectorsDiv)let marks = ['5', '4', '3', '2', dot]marks.forEach(mark => {let markButton = document.createElement('button')markButton.innerText = markmarkButton.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 weightInputCell = document.createElement('td')weightInputCell.classList.add('nst-flex')weightRow.append(weightInputCell)let weightInput = document.createElement('input')weightInput.type = 'number'weightInput.value = weightweightInputCell.append(weightInput)return [templateTable, markInput, weightInput]}// Подсветка оценки/** @param {HTMLDivElement} mark The date */function highlightMark(mark) {mark.animate([{ opacity: 1 },{ opacity: 0 },{ opacity: 1 },{ opacity: 0 },{ opacity: 1 },{ opacity: 0 },{ opacity: 1 }], {duration: 3000,fill: 'forwards'})sortTable()}// Подготовка оценки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)if (assignment.mark && assignment.weight) {let restoreMarkButton = document.createElement('button')restoreMarkButton.innerText = 'Восстановить'controlsDiv.append(restoreMarkButton)restoreMarkButton.addEventListener('click', () => {markInput.value = assignment.markweightInput.value = assignment.weight})}let assignmentMarkButton = document.createElement('button')assignmentMarkButton.innerText = 'Подробности'controlsDiv.append(assignmentMarkButton)assignmentMarkButton.addEventListener('click', () => {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] || keylet value = assignment[key]value = value === true ? "Да" : value === false ? "Нет" : valuelet assignmentRow = document.createElement('tr')assignmentTable.append(assignmentRow)let assignmentLabelCell = document.createElement('td')assignmentLabelCell.innerText = translationassignmentRow.append(assignmentLabelCell)let assignmentInputCell = document.createElement('td')assignmentInputCell.classList.add('nst-flex')assignmentRow.append(assignmentInputCell)let assignmentInputif (key === 'date') {assignmentInput = document.createElement('input')assignmentInput.readOnly = trueassignmentInput.type = 'date'assignmentInput.value = value} else if (typeof value === 'number') {assignmentInput = document.createElement('input')assignmentInput.readOnly = trueassignmentInput.type = 'number'assignmentInput.value = value} else {assignmentInput = document.createElement('div')assignmentInput.innerText = valueassignmentInput.classList.add('nst-area')}assignmentInputCell.append(assignmentInput)}nstModal('Подробности задания', assignmentTable, false)})}nstModal('Редактирование оценки', modalDiv).then(save => {if (save) {markValue.innerText = markInput.valueweightValue.innerText = weightInput.valuehighlightMark(mark)}})})}// Обновление состояния кнопокfunction updateButtons() {if (marksTable.querySelectorAll('.nst-row-selected').length > 0) {addMarkButton.disabled = falsecloneRowButton.disabled = falseremoveRowButton.disabled = false} else {addMarkButton.disabled = truecloneRowButton.disabled = trueremoveRowButton.disabled = true}}updateButtons()function cookRow(row) {let marksCell = row.querySelector('.nst-marks-cell')let totalCell = row.querySelector('.nst-total-cell')// Изменение цвета для оценок и последующий расчет среднего баллаfunction calculateTotalScore() {let markSum = 0let weightSum = 0Array.from(marksCell.children).forEach(markDiv => {let markValue = markDiv.querySelector('.nst-mark-value')let weightValue = markDiv.querySelector('.nst-weight-value')let mark = markValue.innerText.replaceAll(dot, dotMark)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 * weightweightSum += weight})totalCell.innerText = weightSum ? Number((markSum / weightSum).toFixed(2)) : 0totalCell.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 sortTable() {let rows = Array.from(marksTable.rows)// Сортировка строк таблицыif (rowsSortMode != 0) {rows.sort((rowA, rowB) => {let nameA = rowA.querySelector('.nst-name-input').valuelet nameB = rowB.querySelector('.nst-name-input').valuelet marksA = rowA.querySelectorAll('.nst-mark')let marksB = rowB.querySelectorAll('.nst-mark')let totalA = Number(rowA.querySelector('.nst-total-cell').innerText)let totalB = Number(rowB.querySelector('.nst-total-cell').innerText)let dateA = marksA.length > 0 ? JSON.parse(marksA[0].dataset.assignment).date : nulllet dateB = marksB.length > 0 ? JSON.parse(marksB[0].dataset.assignment).date : nullswitch (rowsSortMode) {case 2: return nameB.localeCompare(nameA)case 3: return marksA.length - marksB.lengthcase 4: return marksB.length - marksA.lengthcase 5: return dateA > dateB ? 1 : (dateA < dateB ? -1 : 0)case 6: return dateA < dateB ? 1 : (dateA > dateB ? -1 : 0)case 7: return totalA - totalBcase 8: return totalB - totalAdefault: return nameA.localeCompare(nameB)}})// Обновление таблицыfor (let row of rows) marksTable.appendChild(row)}// Сортировка оценок в каждой строкеif (marksSortMode != 0) {for (let row of rows) {let marks = Array.from(row.querySelectorAll('.nst-mark'))marks.sort((markA, markB) => {let dateA = JSON.parse(markA.dataset.assignment).datelet dateB = JSON.parse(markB.dataset.assignment).datelet valueA = markA.querySelector('.nst-mark-value').innerText === dot ? dotMark : Number(markA.querySelector('.nst-mark-value').innerText)let valueB = markB.querySelector('.nst-mark-value').innerText === dot ? dotMark : Number(markB.querySelector('.nst-mark-value').innerText)let weightA = Number(markA.querySelector('.nst-weight-value').innerText)let weightB = Number(markB.querySelector('.nst-weight-value').innerText)switch (marksSortMode) {case 2: return dateA < dateB ? 1 : (dateA > dateB ? -1 : 0)case 3: return valueA - valueBcase 4: return valueB - valueAcase 5: return weightA - weightBcase 6: return weightB - weightAdefault: return dateA > dateB ? 1 : (dateA < dateB ? -1 : 0)}})// Обновление строкиlet marksContainer = row.querySelector('.nst-marks-cell')for (let mark of marks) marksContainer.appendChild(mark)}}}// Функция для подготовки данных для отправкиfunction flattenJson(json) {let r###lt = {}function flatten(obj, prefix = '') {for (let key in obj) {if (typeof obj[key] === 'object' && obj[key] !== null) {flatten(obj[key], prefix + key + '.')} else {r###lt[prefix + key] = obj[key]}}}flatten(json)return r###lt}fetch('/webapi/grade/assignment/types').then((response) => {return response.json()}).then((types) => {let promises = []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) {let promise = fetch(`/webapi/student/diary/assigns/${assignment.id}`, { 'headers': { 'at': at } }).then((response) => {return response.json()}).then((fullAssignment) => {// Модификация данных в удобный форматfullAssignment.mark = assignment.mark.markif (fullAssignment.mark == null) fullAssignment.mark = dotfullAssignment.studentId = assignment.mark.studentIdfullAssignment.typeId = assignment.typeIdfullAssignment.date = fullAssignment.date.substring(0, 10)let item = types.find(data => data.id == fullAssignment.typeId)fullAssignment.type = item.namefullAssignment = 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)createdMark.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 1000 })return fullAssignment})promises.push(promise)}}}}}return Promise.all(promises)}).then(() => {sortTable()}).catch(err => nstModal('Произошла ошибка', err, false))}).catch(err => nstModal('Произошла ошибка', err, false))}).catch(err => nstModal('Произошла ошибка', err, false))nstModal('Предпросмотр оценок', contentDiv, false)})previewMarksWrapper.append(previewMarksButton)})waitForElement('.top-right-menu').then((element) => {element.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 :not(.preview-marks-wrapper) > input {width: 100%;min-width: 100px;}.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-dialog .nst-radio-wrapper {padding: 4px;}.nst-dialog input[type="radio"] {display: none;}.nst-dialog input[type="radio"] + label {padding-left: 20px;}.nst-dialog input[type="radio"] + label:before {content: "";display: inline-block;position: absolute;margin: 2px;left: 22px;width: 16px;height: 16px;border-radius: 50%;border: 2px solid var(--nst-tertiary);background: var(--nst-quaternary);}.nst-dialog input[type="radio"]:checked + label:before {background-color: var(--nst-primary);border-color: var(--nst-primary);}.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;}.nst-dialog tr {transition: 0.2s;}.nst-dialog tr.nst-row-selected {background: var(--nst-quaternary);}tr.nst-row-selected .nst-total-cell:last-child {background: inherit;}.nst-dialog .nst-marks-table-wrapper {overflow: auto;margin-left: auto;margin-right: auto;border-radius: 24px;max-width: 100%;}.nst-dialog .nst-marks-table-wrapper table {margin-left: initial;margin-right: initial;}.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;}.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;}.nst-dialog ::-webkit-scrollbar {width: 16px;height: 16px;}.nst-dialog ::-webkit-scrollbar-track {background: var(--nst-senary);border-radius: 10px;}.nst-dialog ::-webkit-scrollbar-corner {background: transparent;}.nst-dialog ::-webkit-scrollbar-thumb {background-color: var(--nst-quaternary);border: 4px solid var(--nst-senary);border-radius: 10px;}.nst-dialog ::-webkit-scrollbar-thumb:hover {background-color: var(--nst-tertiary);}.nst-dialog ::-webkit-scrollbar-thumb:active {background-color: var(--nst-secondary);}`)