Greasy Fork is available in English.
Дополнительные инструменты для 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(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 = 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 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.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);
- }
- `)