// ==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);
}
`)