您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Дополнительные инструменты для NetSchool / NetCity (Сетевой Город. Образование)
当前为
// ==UserScript== // @name NetSchool Tweaks // @namespace https://greasyfork.org/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 = 2 function 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 = 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') 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') let passwordInput = document.createElement('input') passwordInput.type = 'password' passwordInput.value = password passwordInput.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 = 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 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 = rowsSortMode sortRowsOptions.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 = index sortRowsOption.id = `sortRows${index}` if (index == rowsSortMode) sortRowsOption.checked = true sortRowsOption.addEventListener('click', () => selectedSortRowsOption = Number(sortRowsOption.value)) sortRowsDiv.append(sortRowsOption) let sortRowsOptionLabel = document.createElement('label') sortRowsOptionLabel.htmlFor = `sortRows${index}` sortRowsOptionLabel.innerText = option sortRowsDiv.append(sortRowsOptionLabel) }) let sortMarksTitle = document.createElement('p') sortMarksTitle.classList.add('nst-label-title') sortMarksTitle.innerText = 'Сортировка оценок' sortDiv.append(sortMarksTitle) let sortMarksOptions = [ 'Не сортировать', 'По дате (по возрастанию)', 'По дате (по убыванию)', 'По оценке (по возрастанию)', 'По оценке (по убыванию)', 'По весу (по возрастанию)', 'По весу (по убыванию)' ] let selectedSortMarksOption = marksSortMode sortMarksOptions.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 = index sortMarksOption.id = `sortMarks${index}` if (index == marksSortMode) sortMarksOption.checked = true sortMarksOption.addEventListener('click', () => selectedSortMarksOption = Number(sortMarksOption.value)) sortMarksDiv.append(sortMarksOption) let sortMarksOptionLabel = document.createElement('label') sortMarksOptionLabel.htmlFor = `sortMarks${index}` sortMarksOptionLabel.innerText = option sortMarksDiv.append(sortMarksOptionLabel) }) nstModal('Сортировка', sortDiv).then(save => { if (save) { rowsSortMode = selectedSortRowsOption GM_setValue('rowsSortMode', rowsSortMode) marksSortMode = selectedSortMarksOption GM_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) return let [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 = 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 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', dot] 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 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] } // Подсветка оценки /** @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.mark weightInput.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] || 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 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(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 * 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 sortTable() { let rows = Array.from(marksTable.rows) // Сортировка строк таблицы if (rowsSortMode != 0) { rows.sort((rowA, rowB) => { let nameA = rowA.querySelector('.nst-name-input').value let nameB = rowB.querySelector('.nst-name-input').value let 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 : null let dateB = marksB.length > 0 ? JSON.parse(marksB[0].dataset.assignment).date : null switch (rowsSortMode) { case 2: return nameB.localeCompare(nameA) case 3: return marksA.length - marksB.length case 4: return marksB.length - marksA.length case 5: return dateA > dateB ? 1 : (dateA < dateB ? -1 : 0) case 6: return dateA < dateB ? 1 : (dateA > dateB ? -1 : 0) case 7: return totalA - totalB case 8: return totalB - totalA default: 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).date let dateB = JSON.parse(markB.dataset.assignment).date let 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 - valueB case 4: return valueB - valueA case 5: return weightA - weightB case 6: return weightB - weightA default: 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 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) => { 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.mark if (fullAssignment.mark == null) fullAssignment.mark = dot 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) 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); } `)