🏠 返回首頁 

Greasy Fork is available in English.

NetSchool Tweaks

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

  1. // ==UserScript==
  2. // @name NetSchool Tweaks
  3. // @namespace https://greasyfork.org/users/843419
  4. // @description Дополнительные инструменты для NetSchool / NetCity (Сетевой Город. Образование)
  5. // @version 1.0.5
  6. // @author Zgoly
  7. // @match *://*/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=ir-tech.ru
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_addStyle
  12. // @license MIT
  13. // ==/UserScript==
  14. try {
  15. console.log(language.Generic.Calendar.kTitle1, 'найден, NetSchool Tweaks активен')
  16. } catch {
  17. return
  18. }
  19. let autoLogin = GM_getValue('autoLogin', false)
  20. let loginName = GM_getValue('loginName', 'Пользователь')
  21. let password = GM_getValue('password', '12345678')
  22. let schoolId = GM_getValue('schoolId', 0)
  23. let autoSkip = GM_getValue('autoSkip', true)
  24. let rowsSortMode = GM_getValue('rowsSortMode', 0)
  25. let marksSortMode = GM_getValue('marksSortMode', 0)
  26. let dot = '•'
  27. let dotMark = 2
  28. function waitForElement(selector) {
  29. return new Promise(resolve => {
  30. if (document.querySelector(selector)) return resolve(document.querySelector(selector))
  31. let observer = new MutationObserver(mutations => {
  32. if (document.querySelector(selector)) {
  33. observer.disconnect()
  34. resolve(document.querySelector(selector))
  35. }
  36. })
  37. observer.observe(document.body, { childList: true, subtree: true })
  38. })
  39. }
  40. if (autoLogin && window.location.pathname.startsWith('/authorize')) {
  41. function runAngularAction() {
  42. try {
  43. angular.element(document.body).scope().$$childTail.$ctrl.loginStrategiesService.loginWithLoginPassCheck(loginName, password, schoolId, null, { 'idpBindUser': 1 })
  44. } catch {
  45. requestAnimationFrame(runAngularAction)
  46. }
  47. }
  48. runAngularAction()
  49. }
  50. waitForElement('ns-modal').then((element) => {
  51. if (autoSkip && element.getAttribute('header') == language.Generic.Login.kTitleSecurityWarning) {
  52. element.querySelector('button').click()
  53. console.log('Модальное окно предупреждения о безопасности пропущено')
  54. }
  55. })
  56. function nstSwitch(parentElement) {
  57. let label = document.createElement('label')
  58. label.classList.add('nst-switch')
  59. let input = document.createElement('input')
  60. input.type = 'checkbox'
  61. input.classList.add('nst-hide')
  62. let div = document.createElement('div')
  63. label.append(input)
  64. label.append(div)
  65. parentElement.append(label)
  66. return input
  67. }
  68. function nstModal(headlineText, contentHTML, showSaveButton = true) {
  69. return new Promise((resolve, reject) => {
  70. let dialog = document.createElement('dialog')
  71. dialog.classList.add('nst-dialog')
  72. let dialogWrapper = document.createElement('div')
  73. dialogWrapper.classList.add('nst-dialog-wrapper')
  74. dialog.append(dialogWrapper)
  75. let headline = document.createElement('p')
  76. headline.classList.add('nst-headline')
  77. headline.textContent = headlineText
  78. dialogWrapper.append(headline)
  79. let dialogAutofocus = document.createElement('input')
  80. dialogAutofocus.autofocus = 'autofocus'
  81. dialogAutofocus.style.display = 'none'
  82. dialogWrapper.append(dialogAutofocus)
  83. let content = document.createElement('div')
  84. content.classList.add('nst-content')
  85. content.append(contentHTML)
  86. dialogWrapper.append(content)
  87. let actions = document.createElement('div')
  88. actions.classList.add('nst-actions')
  89. let closeButton = document.createElement('button')
  90. closeButton.classList.add('nst-close')
  91. closeButton.textContent = 'Закрыть'
  92. closeButton.addEventListener('click', () => closeDialog(false))
  93. actions.append(closeButton)
  94. if (showSaveButton) {
  95. let saveButton = document.createElement('button')
  96. saveButton.classList.add('nst-save')
  97. saveButton.textContent = 'Сохранить'
  98. saveButton.addEventListener('click', () => closeDialog(true))
  99. actions.append(saveButton)
  100. }
  101. dialogWrapper.append(actions)
  102. document.body.append(dialog)
  103. dialog.showModal()
  104. // Убираем фокус с поля ввода
  105. document.activeElement.blur()
  106. document.body.classList.add('nst-no-scroll')
  107. function closeDialog(r###lt = false) {
  108. dialog.classList.add('nst-hide-dialog')
  109. setTimeout(() => {
  110. dialog.remove()
  111. if (document.getElementsByTagName('dialog').length < 1) document.body.classList.remove('nst-no-scroll')
  112. }, 500)
  113. resolve(r###lt)
  114. }
  115. contentHTML.closeDialog = closeDialog
  116. dialog.addEventListener('click', (event) => {
  117. if (event.target === dialog) {
  118. closeDialog(false)
  119. }
  120. })
  121. dialog.addEventListener('close', () => closeDialog())
  122. dialog.addEventListener('error', reject)
  123. })
  124. }
  125. let settings = document.createElement('li')
  126. let settingsLink = document.createElement('a')
  127. settings.append(settingsLink)
  128. let settingsBody = document.createElement('span')
  129. settingsBody.classList.add('cb-settings')
  130. settingsLink.append(settingsBody)
  131. let settingsIcon = document.createElement('i')
  132. settingsIcon.classList.add('icon-gear', 'nst-settings-icon')
  133. settingsBody.append(settingsIcon)
  134. settingsBody.addEventListener('click', () => {
  135. let div = document.createElement('div')
  136. let table = document.createElement('table')
  137. // Переключатель авто входа
  138. let autoLoginRow = document.createElement('tr')
  139. let autoLoginLabelCell = document.createElement('td')
  140. let autoLoginLabelTitle = document.createElement('div')
  141. autoLoginLabelTitle.classList.add('nst-label-title')
  142. autoLoginLabelTitle.textContent = 'Авто вход'
  143. autoLoginLabelCell.append(autoLoginLabelTitle)
  144. let autoLoginLabelDescription = document.createElement('div')
  145. autoLoginLabelDescription.classList.add('nst-label-description')
  146. autoLoginLabelDescription.textContent = 'Авто вход по логину и паролю.'
  147. autoLoginLabelCell.append(autoLoginLabelDescription)
  148. let autoLoginInputCell = document.createElement('td')
  149. let autoLoginInput = nstSwitch(autoLoginInputCell)
  150. autoLoginInput.checked = autoLogin
  151. autoLoginRow.append(autoLoginLabelCell)
  152. autoLoginRow.append(autoLoginInputCell)
  153. table.append(autoLoginRow)
  154. // Поле логина
  155. let loginNameRow = document.createElement('tr')
  156. let loginNameLabelCell = document.createElement('td')
  157. let loginNameLabelTitle = document.createElement('div')
  158. loginNameLabelTitle.classList.add('nst-label-title')
  159. loginNameLabelTitle.textContent = 'Логин'
  160. loginNameLabelCell.append(loginNameLabelTitle)
  161. let loginNameLabelDescription = document.createElement('div')
  162. loginNameLabelDescription.classList.add('nst-label-description')
  163. loginNameLabelDescription.textContent = 'Логин для входа.'
  164. loginNameLabelCell.append(loginNameLabelDescription)
  165. let loginNameInputCell = document.createElement('td')
  166. let loginNameInput = document.createElement('input')
  167. loginNameInput.type = 'text'
  168. loginNameInput.value = loginName
  169. loginNameInputCell.append(loginNameInput)
  170. loginNameRow.append(loginNameLabelCell)
  171. loginNameRow.append(loginNameInputCell)
  172. table.append(loginNameRow)
  173. // Поле пароля
  174. let passwordRow = document.createElement('tr')
  175. let passwordLabelCell = document.createElement('td')
  176. let passwordLabelTitle = document.createElement('div')
  177. passwordLabelTitle.classList.add('nst-label-title')
  178. passwordLabelTitle.textContent = 'Пароль'
  179. passwordLabelCell.append(passwordLabelTitle)
  180. let passwordLabelDescription = document.createElement('div')
  181. passwordLabelDescription.classList.add('nst-label-description')
  182. passwordLabelDescription.textContent = 'Пароль для входа.'
  183. passwordLabelCell.append(passwordLabelDescription)
  184. let passwordInputCell = document.createElement('td')
  185. let passwordInput = document.createElement('input')
  186. passwordInput.type = 'password'
  187. passwordInput.value = password
  188. passwordInput.addEventListener('focus', () => passwordInput.type = 'text')
  189. passwordInput.addEventListener('blur', () => passwordInput.type = 'password')
  190. passwordInputCell.append(passwordInput)
  191. passwordRow.append(passwordLabelCell)
  192. passwordRow.append(passwordInputCell)
  193. table.append(passwordRow)
  194. // Поле ID школы
  195. let schoolIdRow = document.createElement('tr')
  196. let schoolIdLabelCell = document.createElement('td')
  197. let schoolIdLabelTitle = document.createElement('div')
  198. schoolIdLabelTitle.classList.add('nst-label-title')
  199. schoolIdLabelTitle.textContent = 'ID школы'
  200. schoolIdLabelCell.append(schoolIdLabelTitle)
  201. let schoolIdLabelDescription = document.createElement('div')
  202. schoolIdLabelDescription.classList.add('nst-label-description')
  203. schoolIdLabelDescription.textContent = 'ID школы для входа. Оставьте пустым, если не знаете.'
  204. schoolIdLabelCell.append(schoolIdLabelDescription)
  205. let schoolIdInputCell = document.createElement('td')
  206. let schoolIdInput = document.createElement('input')
  207. schoolIdInput.type = 'number'
  208. schoolIdInput.value = schoolId
  209. schoolIdInput.placeholder = schoolId
  210. schoolIdInputCell.append(schoolIdInput)
  211. schoolIdRow.append(schoolIdLabelCell)
  212. schoolIdRow.append(schoolIdInputCell)
  213. table.append(schoolIdRow)
  214. // Переключатель авто пропуска
  215. let autoSkipRow = document.createElement('tr')
  216. let autoSkipLabelCell = document.createElement('td')
  217. let autoSkipLabelTitle = document.createElement('div')
  218. autoSkipLabelTitle.classList.add('nst-label-title')
  219. autoSkipLabelTitle.textContent = 'Авто пропуск'
  220. autoSkipLabelCell.append(autoSkipLabelTitle)
  221. let autoSkipLabelDescription = document.createElement('div')
  222. autoSkipLabelDescription.classList.add('nst-label-description')
  223. autoSkipLabelDescription.textContent = 'Авто пропуск навязчивых уведомлений.'
  224. autoSkipLabelCell.append(autoSkipLabelDescription)
  225. let autoSkipInputCell = document.createElement('td')
  226. let autoSkipInput = nstSwitch(autoSkipInputCell)
  227. autoSkipInput.checked = autoSkip
  228. autoSkipRow.append(autoSkipLabelCell)
  229. autoSkipRow.append(autoSkipInputCell)
  230. table.append(autoSkipRow)
  231. function toggleFields() {
  232. let fields = [loginNameInput, passwordInput, schoolIdInput]
  233. fields.forEach(field => {
  234. field.disabled = !autoLoginInput.checked
  235. })
  236. }
  237. toggleFields()
  238. autoLoginInput.addEventListener('change', toggleFields)
  239. div.append(table)
  240. // Сохранение настроек
  241. nstModal('Настройки', div).then(save => {
  242. if (save) {
  243. GM_setValue('autoLogin', autoLoginInput.checked)
  244. autoLogin = autoLoginInput.checked
  245. GM_setValue('loginName', loginNameInput.value)
  246. loginName = loginNameInput.value
  247. GM_setValue('password', passwordInput.value)
  248. password = passwordInput.value
  249. GM_setValue('schoolId', schoolIdInput.value == '' ? appContext.schoolId : schoolIdInput.value)
  250. schoolId = schoolIdInput.value == '' ? appContext.schoolId : schoolIdInput.value
  251. GM_setValue('autoSkip', autoSkipInput.checked)
  252. autoSkip = autoSkipInput.checked
  253. }
  254. })
  255. let previewMarksWrapper = document.createElement('div')
  256. previewMarksWrapper.classList.add('preview-marks-wrapper')
  257. div.append(previewMarksWrapper)
  258. let at = appContext.at
  259. let weekStart = appContext.weekStart
  260. let weekEnd = appContext.weekEnd
  261. // Начало учебного года
  262. let startDateInput = document.createElement('input')
  263. startDateInput.type = 'date'
  264. startDateInput.value = weekStart
  265. previewMarksWrapper.append(startDateInput)
  266. // Конец учебного года
  267. let endDateInput = document.createElement('input')
  268. endDateInput.type = 'date'
  269. endDateInput.value = weekEnd
  270. previewMarksWrapper.append(endDateInput)
  271. if (weekStart == undefined || weekEnd == undefined) {
  272. fetch('/webapi/v2/reports/studenttotal', { 'headers': { 'at': at } }).then((response) => {
  273. return response.json()
  274. }).then((data) => {
  275. weekStart = data.filterSources[3].defaultRange.start.substring(0, 10)
  276. startDateInput.value = weekStart
  277. weekEnd = data.filterSources[3].defaultRange.end.substring(0, 10)
  278. endDateInput.value = weekEnd
  279. })
  280. }
  281. // Кнопка предпросмотра оценок
  282. let previewMarksButton = document.createElement('button')
  283. previewMarksButton.innerText = 'Предпросмотр оценок'
  284. previewMarksButton.addEventListener('click', () => {
  285. let marksTableWrapper = document.createElement('div')
  286. marksTableWrapper.classList.add('nst-marks-table-wrapper')
  287. let contentDiv = document.createElement('div')
  288. contentDiv.classList.add('nst-content')
  289. contentDiv.append(marksTableWrapper)
  290. fetch('/webapi/student/diary/init', { 'headers': { 'at': at } }).then((response) => {
  291. return response.json()
  292. }).then((data) => {
  293. let studentId = data.students[0].studentId
  294. let yearId = appContext.yearId
  295. let startDate = startDateInput.value
  296. let endDate = endDateInput.value
  297. // Запрос дневика
  298. fetch(`/webapi/student/diary?studentId=${studentId}&weekStart=${startDate}&weekEnd=${endDate}&withLaAssigns=true&yearId=${yearId}`, { 'headers': { 'at': at } }).then((response) => {
  299. return response.json()
  300. }).then((data) => {
  301. // Повторный запрос дневника (с текущей датой для отображения правильной недели, игнорируется)
  302. let currentDate = date2strf(new Date(), 'yyyy\x01mm\x01dd\x01.')
  303. fetch(`/webapi/student/diary?studentId=${studentId}&weekStart=${currentDate}&weekEnd=${currentDate}&withLaAssigns=true&yearId=${yearId}`, { 'headers': { 'at': at } })
  304. let marksTable = document.createElement('table')
  305. marksTableWrapper.append(marksTable)
  306. let tableControlsDiv = document.createElement('div')
  307. tableControlsDiv.classList.add('nst-controls')
  308. contentDiv.append(tableControlsDiv)
  309. // Кнопка для выбора режима сортировки
  310. let sortButton = document.createElement('button')
  311. sortButton.innerText = 'Сортировать'
  312. sortButton.addEventListener('click', () => {
  313. let sortDiv = document.createElement('div')
  314. let sortRowsTitle = document.createElement('p')
  315. sortRowsTitle.classList.add('nst-label-title')
  316. sortRowsTitle.innerText = 'Сортировка строк'
  317. sortDiv.append(sortRowsTitle)
  318. let sortRowsOptions = [
  319. 'Не сортировать',
  320. 'По имени (по возрастанию)',
  321. 'По имени (по убыванию)',
  322. 'По количеству оценок (по возрастанию)',
  323. 'По количеству оценок (по убыванию)',
  324. 'По дате (по возрастанию)',
  325. 'По дате (по убыванию)',
  326. 'По среднему баллу (по возрастанию)',
  327. 'По среднему баллу (по убыванию)'
  328. ]
  329. let selectedSortRowsOption = rowsSortMode
  330. sortRowsOptions.forEach((option, index) => {
  331. let sortRowsDiv = document.createElement('div')
  332. sortRowsDiv.classList.add('nst-radio-wrapper')
  333. sortDiv.append(sortRowsDiv)
  334. let sortRowsOption = document.createElement('input')
  335. sortRowsOption.type = 'radio'
  336. sortRowsOption.name = 'sortRows'
  337. sortRowsOption.value = index
  338. sortRowsOption.id = `sortRows${index}`
  339. if (index == rowsSortMode) sortRowsOption.checked = true
  340. sortRowsOption.addEventListener('click', () => selectedSortRowsOption = Number(sortRowsOption.value))
  341. sortRowsDiv.append(sortRowsOption)
  342. let sortRowsOptionLabel = document.createElement('label')
  343. sortRowsOptionLabel.htmlFor = `sortRows${index}`
  344. sortRowsOptionLabel.innerText = option
  345. sortRowsDiv.append(sortRowsOptionLabel)
  346. })
  347. let sortMarksTitle = document.createElement('p')
  348. sortMarksTitle.classList.add('nst-label-title')
  349. sortMarksTitle.innerText = 'Сортировка оценок'
  350. sortDiv.append(sortMarksTitle)
  351. let sortMarksOptions = [
  352. 'Не сортировать',
  353. 'По дате (по возрастанию)',
  354. 'По дате (по убыванию)',
  355. 'По оценке (по возрастанию)',
  356. 'По оценке (по убыванию)',
  357. 'По весу (по возрастанию)',
  358. 'По весу (по убыванию)'
  359. ]
  360. let selectedSortMarksOption = marksSortMode
  361. sortMarksOptions.forEach((option, index) => {
  362. let sortMarksDiv = document.createElement('div')
  363. sortMarksDiv.classList.add('nst-radio-wrapper')
  364. sortDiv.append(sortMarksDiv)
  365. let sortMarksOption = document.createElement('input')
  366. sortMarksOption.type = 'radio'
  367. sortMarksOption.name = 'sortMarks'
  368. sortMarksOption.value = index
  369. sortMarksOption.id = `sortMarks${index}`
  370. if (index == marksSortMode) sortMarksOption.checked = true
  371. sortMarksOption.addEventListener('click', () => selectedSortMarksOption = Number(sortMarksOption.value))
  372. sortMarksDiv.append(sortMarksOption)
  373. let sortMarksOptionLabel = document.createElement('label')
  374. sortMarksOptionLabel.htmlFor = `sortMarks${index}`
  375. sortMarksOptionLabel.innerText = option
  376. sortMarksDiv.append(sortMarksOptionLabel)
  377. })
  378. nstModal('Сортировка', sortDiv).then(save => {
  379. if (save) {
  380. rowsSortMode = selectedSortRowsOption
  381. GM_setValue('rowsSortMode', rowsSortMode)
  382. marksSortMode = selectedSortMarksOption
  383. GM_setValue('marksSortMode', marksSortMode)
  384. sortTable()
  385. }
  386. })
  387. })
  388. tableControlsDiv.append(sortButton)
  389. // Кнопка для выбора
  390. let selectAllButton = document.createElement('button')
  391. selectAllButton.innerText = 'Выбрать все'
  392. selectAllButton.addEventListener('click', () => {
  393. for (let row of marksTable.rows) {
  394. row.classList.add('nst-row-selected')
  395. }
  396. updateButtons()
  397. })
  398. tableControlsDiv.append(selectAllButton)
  399. // Кнопка для отмены выбора
  400. let deselectAllButton = document.createElement('button')
  401. deselectAllButton.innerText = 'Отменить выбор'
  402. deselectAllButton.addEventListener('click', () => {
  403. for (let row of marksTable.rows) {
  404. row.classList.remove('nst-row-selected')
  405. }
  406. updateButtons()
  407. })
  408. tableControlsDiv.append(deselectAllButton)
  409. // Кнопка для создания строки
  410. let addRowButton = document.createElement('button')
  411. addRowButton.innerText = 'Cоздать'
  412. addRowButton.addEventListener('click', () => {
  413. let selectedRows = marksTable.querySelectorAll('.nst-row-selected')
  414. let newRow = createRow()
  415. if (selectedRows.length > 0) {
  416. selectedRows[selectedRows.length - 1].after(newRow)
  417. selectedRows.forEach(row => row.classList.remove('nst-row-selected'))
  418. } else {
  419. marksTable.prepend(newRow)
  420. }
  421. cookRow(newRow)
  422. newRow.classList.add('nst-row-selected')
  423. newRow.scrollIntoView({ behavior: "smooth" })
  424. updateButtons()
  425. })
  426. tableControlsDiv.append(addRowButton)
  427. // Кнопка для клонирования строки
  428. let cloneRowButton = document.createElement('button')
  429. cloneRowButton.innerText = 'Клонировать'
  430. cloneRowButton.addEventListener('click', () => {
  431. let selectedRows = marksTable.querySelectorAll('.nst-row-selected')
  432. selectedRows.forEach(row => {
  433. let clonedRow = row.cloneNode(true)
  434. row.after(clonedRow)
  435. cookRow(clonedRow)
  436. let marksCell = clonedRow.querySelector('.nst-marks-cell')
  437. Array.from(marksCell.children).forEach(markDiv => {
  438. cookMark(markDiv)
  439. })
  440. row.classList.remove('nst-row-selected')
  441. })
  442. updateButtons()
  443. })
  444. tableControlsDiv.append(cloneRowButton)
  445. // Кнопка для удаления строки
  446. let removeRowButton = document.createElement('button')
  447. removeRowButton.innerText = 'Удалить'
  448. removeRowButton.addEventListener('click', () => {
  449. let selectedRows = marksTable.querySelectorAll('.nst-row-selected')
  450. selectedRows.forEach(row => row.remove())
  451. updateButtons()
  452. })
  453. tableControlsDiv.append(removeRowButton)
  454. // Кнопка для добавления оценки
  455. let addMarkButton = document.createElement('button')
  456. addMarkButton.innerText = 'Добавить оценку'
  457. addMarkButton.addEventListener('click', () => {
  458. let selectedRows = marksTable.querySelectorAll('.nst-row-selected')
  459. if (selectedRows.length == 0) return
  460. let [templateTable, markInput, weightInput] = markModalTemplate(5, 20)
  461. nstModal('Добавление оценки', templateTable).then(save => {
  462. if (save) {
  463. selectedRows.forEach(row => {
  464. let mark = createMark(markInput.value, weightInput.value)
  465. mark.dataset.assignment = JSON.stringify({ "date": new Date().toLocaleDateString("en-CA") })
  466. row.querySelector('.nst-marks-cell').append(mark)
  467. cookMark(mark)
  468. highlightMark(mark)
  469. row.classList.remove('nst-row-selected')
  470. })
  471. updateButtons()
  472. }
  473. })
  474. })
  475. tableControlsDiv.append(addMarkButton)
  476. // Функция для создания оценки
  477. function createMark(mark, weight, fullAssignment = null) {
  478. let markDiv = document.createElement('div')
  479. if (fullAssignment) markDiv.dataset.assignment = JSON.stringify(fullAssignment)
  480. markDiv.classList.add('nst-mark')
  481. let markValue = document.createElement('p')
  482. markValue.innerText = mark
  483. markValue.classList.add('nst-mark-value')
  484. markDiv.append(markValue)
  485. let weightValue = document.createElement('p')
  486. weightValue.innerText = weight
  487. weightValue.classList.add('nst-weight-value')
  488. markDiv.append(weightValue)
  489. return markDiv
  490. }
  491. // Функция для создания строки
  492. function createRow(name = '') {
  493. let row = document.createElement('tr')
  494. let nameCell = document.createElement('td')
  495. let nameInput = document.createElement('input')
  496. nameInput.value = name
  497. nameInput.classList.add('nst-name-input')
  498. nameInput.placeholder = "Имя предмета"
  499. nameCell.append(nameInput)
  500. row.append(nameCell)
  501. let marksCell = document.createElement('td')
  502. marksCell.classList.add('nst-marks-cell')
  503. row.append(marksCell)
  504. let totalCell = document.createElement('td')
  505. totalCell.classList.add('nst-total-cell')
  506. row.append(totalCell)
  507. return row
  508. }
  509. // Функция для создания шаблона модального окна
  510. function markModalTemplate(mark, weight) {
  511. let templateTable = document.createElement('table')
  512. let markRow = document.createElement('tr')
  513. templateTable.append(markRow)
  514. let markLabelCell = document.createElement('td')
  515. markRow.append(markLabelCell)
  516. let markLabelTitle = document.createElement('div')
  517. markLabelTitle.classList.add('nst-label-title')
  518. markLabelTitle.textContent = 'Оценка'
  519. markLabelCell.append(markLabelTitle)
  520. let markInputCell = document.createElement('td')
  521. markInputCell.classList.add('nst-flex')
  522. markRow.append(markInputCell)
  523. let markInput = document.createElement('input')
  524. markInput.readOnly = true
  525. markInput.value = mark
  526. markInputCell.append(markInput)
  527. let markSelectorsDiv = document.createElement('div')
  528. markSelectorsDiv.classList.add('nst-mark-selectors')
  529. markInputCell.append(markSelectorsDiv)
  530. let marks = ['5', '4', '3', '2', dot]
  531. marks.forEach(mark => {
  532. let markButton = document.createElement('button')
  533. markButton.innerText = mark
  534. markButton.addEventListener('click', () => markInput.value = mark)
  535. markSelectorsDiv.append(markButton)
  536. })
  537. let weightRow = document.createElement('tr')
  538. templateTable.append(weightRow)
  539. let weightLabelCell = document.createElement('td')
  540. weightRow.append(weightLabelCell)
  541. let weightLabelTitle = document.createElement('div')
  542. weightLabelTitle.classList.add('nst-label-title')
  543. weightLabelTitle.textContent = 'Вес'
  544. weightLabelCell.append(weightLabelTitle)
  545. let weightInputCell = document.createElement('td')
  546. weightInputCell.classList.add('nst-flex')
  547. weightRow.append(weightInputCell)
  548. let weightInput = document.createElement('input')
  549. weightInput.type = 'number'
  550. weightInput.value = weight
  551. weightInputCell.append(weightInput)
  552. return [templateTable, markInput, weightInput]
  553. }
  554. // Подсветка оценки
  555. /** @param {HTMLDivElement} mark The date */
  556. function highlightMark(mark) {
  557. mark.animate([
  558. { opacity: 1 },
  559. { opacity: 0 },
  560. { opacity: 1 },
  561. { opacity: 0 },
  562. { opacity: 1 },
  563. { opacity: 0 },
  564. { opacity: 1 }
  565. ], {
  566. duration: 3000,
  567. fill: 'forwards'
  568. })
  569. sortTable()
  570. }
  571. // Подготовка оценки
  572. function cookMark(mark) {
  573. let markValue = mark.querySelector('.nst-mark-value')
  574. let weightValue = mark.querySelector('.nst-weight-value')
  575. mark.addEventListener('click', () => {
  576. let modalDiv = document.createElement('div')
  577. let [templateTable, markInput, weightInput] = markModalTemplate(markValue.innerText, weightValue.innerText)
  578. modalDiv.append(templateTable)
  579. let controlsDiv = document.createElement('div')
  580. controlsDiv.classList.add('nst-controls')
  581. modalDiv.append(controlsDiv)
  582. let cloneMarkButton = document.createElement('button')
  583. cloneMarkButton.innerText = 'Клонировать'
  584. controlsDiv.append(cloneMarkButton)
  585. cloneMarkButton.addEventListener('click', () => {
  586. modalDiv.closeDialog(true)
  587. let newMark = mark.cloneNode(true)
  588. mark.after(newMark)
  589. cookMark(newMark)
  590. highlightMark(newMark)
  591. })
  592. let deleteMarkButton = document.createElement('button')
  593. deleteMarkButton.innerText = 'Удалить'
  594. controlsDiv.append(deleteMarkButton)
  595. deleteMarkButton.addEventListener('click', () => {
  596. mark.remove()
  597. modalDiv.closeDialog(false)
  598. })
  599. if (mark.dataset.assignment) {
  600. let assignment = JSON.parse(mark.dataset.assignment)
  601. if (assignment.mark && assignment.weight) {
  602. let restoreMarkButton = document.createElement('button')
  603. restoreMarkButton.innerText = 'Восстановить'
  604. controlsDiv.append(restoreMarkButton)
  605. restoreMarkButton.addEventListener('click', () => {
  606. markInput.value = assignment.mark
  607. weightInput.value = assignment.weight
  608. })
  609. }
  610. let assignmentMarkButton = document.createElement('button')
  611. assignmentMarkButton.innerText = 'Подробности'
  612. controlsDiv.append(assignmentMarkButton)
  613. assignmentMarkButton.addEventListener('click', () => {
  614. let assignmentTable = document.createElement('table')
  615. let translations = {
  616. 'id': 'ID задания',
  617. 'assignmentName': 'Тема задания',
  618. 'activityName': 'Имя деятельности',
  619. 'problemName': 'Название задачи',
  620. 'studentId': 'ID ученика',
  621. 'subjectGroup.id': 'ID предмета',
  622. 'subjectGroup.name': 'Название предмета',
  623. 'teachers.0.id': 'ID учителя',
  624. 'teachers.0.name': 'Имя учителя',
  625. 'productId': 'ID продукта',
  626. 'isDeleted': 'Удалено',
  627. 'weight': 'Вес',
  628. 'date': 'Дата',
  629. 'description': 'Описание',
  630. 'mark': 'Оценка',
  631. 'typeId': 'ID типа задания',
  632. 'type': 'Тип задания'
  633. }
  634. for (let key in assignment) {
  635. let translation = translations[key] || key
  636. let value = assignment[key]
  637. value = value === true ? "Да" : value === false ? "Нет" : value
  638. let assignmentRow = document.createElement('tr')
  639. assignmentTable.append(assignmentRow)
  640. let assignmentLabelCell = document.createElement('td')
  641. assignmentLabelCell.innerText = translation
  642. assignmentRow.append(assignmentLabelCell)
  643. let assignmentInputCell = document.createElement('td')
  644. assignmentInputCell.classList.add('nst-flex')
  645. assignmentRow.append(assignmentInputCell)
  646. let assignmentInput
  647. if (key === 'date') {
  648. assignmentInput = document.createElement('input')
  649. assignmentInput.readOnly = true
  650. assignmentInput.type = 'date'
  651. assignmentInput.value = value
  652. } else if (typeof value === 'number') {
  653. assignmentInput = document.createElement('input')
  654. assignmentInput.readOnly = true
  655. assignmentInput.type = 'number'
  656. assignmentInput.value = value
  657. } else {
  658. assignmentInput = document.createElement('div')
  659. assignmentInput.innerText = value
  660. assignmentInput.classList.add('nst-area')
  661. }
  662. assignmentInputCell.append(assignmentInput)
  663. }
  664. nstModal('Подробности задания', assignmentTable, false)
  665. })
  666. }
  667. nstModal('Редактирование оценки', modalDiv).then(save => {
  668. if (save) {
  669. markValue.innerText = markInput.value
  670. weightValue.innerText = weightInput.value
  671. highlightMark(mark)
  672. }
  673. })
  674. })
  675. }
  676. // Обновление состояния кнопок
  677. function updateButtons() {
  678. if (marksTable.querySelectorAll('.nst-row-selected').length > 0) {
  679. addMarkButton.disabled = false
  680. cloneRowButton.disabled = false
  681. removeRowButton.disabled = false
  682. } else {
  683. addMarkButton.disabled = true
  684. cloneRowButton.disabled = true
  685. removeRowButton.disabled = true
  686. }
  687. }
  688. updateButtons()
  689. function cookRow(row) {
  690. let marksCell = row.querySelector('.nst-marks-cell')
  691. let totalCell = row.querySelector('.nst-total-cell')
  692. // Изменение цвета для оценок и последующий расчет среднего балла
  693. function calculateTotalScore() {
  694. let markSum = 0
  695. let weightSum = 0
  696. Array.from(marksCell.children).forEach(markDiv => {
  697. let markValue = markDiv.querySelector('.nst-mark-value')
  698. let weightValue = markDiv.querySelector('.nst-weight-value')
  699. let mark = markValue.innerText.replaceAll(dot, dotMark)
  700. let weight = Number(weightValue.innerText)
  701. markValue.classList.remove(...['nst-mark-excellent', 'nst-mark-good', 'nst-mark-average', 'nst-mark-bad'])
  702. markValue.classList.add(getMarkClass(mark))
  703. markSum += mark * weight
  704. weightSum += weight
  705. })
  706. totalCell.innerText = weightSum ? Number((markSum / weightSum).toFixed(2)) : 0
  707. totalCell.classList.remove(...['nst-mark-excellent', 'nst-mark-good', 'nst-mark-average', 'nst-mark-bad'])
  708. totalCell.classList.add(getMarkClass(totalCell.innerText))
  709. }
  710. // Получение цвета оценки
  711. function getMarkClass(mark) {
  712. 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'
  713. }
  714. let observer = new MutationObserver(() => {
  715. calculateTotalScore()
  716. })
  717. observer.observe(marksCell, { childList: true, subtree: true })
  718. calculateTotalScore()
  719. // Выделение строки при нажатии на балл
  720. totalCell.addEventListener('click', () => {
  721. row.classList.toggle('nst-row-selected')
  722. updateButtons()
  723. })
  724. }
  725. // Сортировка таблицы
  726. function sortTable() {
  727. let rows = Array.from(marksTable.rows)
  728. // Сортировка строк таблицы
  729. if (rowsSortMode != 0) {
  730. rows.sort((rowA, rowB) => {
  731. let nameA = rowA.querySelector('.nst-name-input').value
  732. let nameB = rowB.querySelector('.nst-name-input').value
  733. let marksA = rowA.querySelectorAll('.nst-mark')
  734. let marksB = rowB.querySelectorAll('.nst-mark')
  735. let totalA = Number(rowA.querySelector('.nst-total-cell').innerText)
  736. let totalB = Number(rowB.querySelector('.nst-total-cell').innerText)
  737. let dateA = marksA.length > 0 ? JSON.parse(marksA[0].dataset.assignment).date : null
  738. let dateB = marksB.length > 0 ? JSON.parse(marksB[0].dataset.assignment).date : null
  739. switch (rowsSortMode) {
  740. case 2: return nameB.localeCompare(nameA)
  741. case 3: return marksA.length - marksB.length
  742. case 4: return marksB.length - marksA.length
  743. case 5: return dateA > dateB ? 1 : (dateA < dateB ? -1 : 0)
  744. case 6: return dateA < dateB ? 1 : (dateA > dateB ? -1 : 0)
  745. case 7: return totalA - totalB
  746. case 8: return totalB - totalA
  747. default: return nameA.localeCompare(nameB)
  748. }
  749. })
  750. // Обновление таблицы
  751. for (let row of rows) marksTable.appendChild(row)
  752. }
  753. // Сортировка оценок в каждой строке
  754. if (marksSortMode != 0) {
  755. for (let row of rows) {
  756. let marks = Array.from(row.querySelectorAll('.nst-mark'))
  757. marks.sort((markA, markB) => {
  758. let dateA = JSON.parse(markA.dataset.assignment).date
  759. let dateB = JSON.parse(markB.dataset.assignment).date
  760. let valueA = markA.querySelector('.nst-mark-value').innerText === dot ? dotMark : Number(markA.querySelector('.nst-mark-value').innerText)
  761. let valueB = markB.querySelector('.nst-mark-value').innerText === dot ? dotMark : Number(markB.querySelector('.nst-mark-value').innerText)
  762. let weightA = Number(markA.querySelector('.nst-weight-value').innerText)
  763. let weightB = Number(markB.querySelector('.nst-weight-value').innerText)
  764. switch (marksSortMode) {
  765. case 2: return dateA < dateB ? 1 : (dateA > dateB ? -1 : 0)
  766. case 3: return valueA - valueB
  767. case 4: return valueB - valueA
  768. case 5: return weightA - weightB
  769. case 6: return weightB - weightA
  770. default: return dateA > dateB ? 1 : (dateA < dateB ? -1 : 0)
  771. }
  772. })
  773. // Обновление строки
  774. let marksContainer = row.querySelector('.nst-marks-cell')
  775. for (let mark of marks) marksContainer.appendChild(mark)
  776. }
  777. }
  778. }
  779. // Функция для подготовки данных для отправки
  780. function flattenJson(json) {
  781. let r###lt = {}
  782. function flatten(obj, prefix = '') {
  783. for (let key in obj) {
  784. if (typeof obj[key] === 'object' && obj[key] !== null) {
  785. flatten(obj[key], prefix + key + '.')
  786. } else {
  787. r###lt[prefix + key] = obj[key]
  788. }
  789. }
  790. }
  791. flatten(json)
  792. return r###lt
  793. }
  794. fetch('/webapi/grade/assignment/types').then((response) => {
  795. return response.json()
  796. }).then((types) => {
  797. let promises = []
  798. for (let day of data.weekDays) {
  799. for (let lesson of day.lessons) {
  800. if (Array.isArray(lesson.assignments)) {
  801. for (let assignment of lesson.assignments) {
  802. if (assignment.mark) {
  803. let promise = fetch(`/webapi/student/diary/assigns/${assignment.id}`, { 'headers': { 'at': at } }).then((response) => {
  804. return response.json()
  805. }).then((fullAssignment) => {
  806. // Модификация данных в удобный формат
  807. fullAssignment.mark = assignment.mark.mark
  808. if (fullAssignment.mark == null) fullAssignment.mark = dot
  809. fullAssignment.studentId = assignment.mark.studentId
  810. fullAssignment.typeId = assignment.typeId
  811. fullAssignment.date = fullAssignment.date.substring(0, 10)
  812. let item = types.find(data => data.id == fullAssignment.typeId)
  813. fullAssignment.type = item.name
  814. fullAssignment = flattenJson(fullAssignment)
  815. // Удаление ненужных полей
  816. for (let key in fullAssignment) {
  817. if (fullAssignment[key] == null) delete fullAssignment[key]
  818. }
  819. // Объявление / создание ряда
  820. let row = Array.from(marksTable.rows).find(r => r.querySelector('.nst-name-input').value == lesson.subjectName)
  821. if (!row) {
  822. row = createRow(lesson.subjectName)
  823. marksTable.append(row)
  824. cookRow(row)
  825. }
  826. // Добавление оценки
  827. let createdMark = createMark(fullAssignment.mark, fullAssignment.weight, fullAssignment)
  828. row.querySelector('.nst-marks-cell').append(createdMark)
  829. cookMark(createdMark)
  830. createdMark.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 1000 })
  831. return fullAssignment
  832. })
  833. promises.push(promise)
  834. }
  835. }
  836. }
  837. }
  838. }
  839. return Promise.all(promises)
  840. }).then(() => {
  841. sortTable()
  842. }).catch(err => nstModal('Произошла ошибка', err, false))
  843. }).catch(err => nstModal('Произошла ошибка', err, false))
  844. }).catch(err => nstModal('Произошла ошибка', err, false))
  845. nstModal('Предпросмотр оценок', contentDiv, false)
  846. })
  847. previewMarksWrapper.append(previewMarksButton)
  848. })
  849. waitForElement('.top-right-menu').then((element) => {
  850. element.prepend(settings)
  851. })
  852. GM_addStyle(`
  853. @import url('https://fonts.googleapis.com/css2?family=Nunito&display=swap');
  854. :root {
  855. --nst-primary: #64a0c8;
  856. --nst-secondary: #415f78;
  857. --nst-tertiary: #374b5f;
  858. --nst-quaternary: #232a32;
  859. --nst-quinary: #1a1c1e;
  860. --nst-senary: #141618;
  861. --nst-text-primary: #c8e6ff;
  862. --nst-text-secondary: #aadcff;
  863. --nst-text-tertiary: #87afc8;
  864. }
  865. .nst-no-scroll {
  866. touch-action: none;
  867. overflow: hidden;
  868. }
  869. .nst-flex {
  870. display: flex;
  871. flex-direction: column;
  872. }
  873. .nst-flex input {
  874. flex: 1;
  875. }
  876. .nst-settings-icon {
  877. display: flex !important;
  878. justify-content: center;
  879. color: white;
  880. scale: 0.75;
  881. }
  882. .nst-dialog {
  883. border: none;
  884. outline: none;
  885. background: var(--nst-quinary);
  886. border-radius: 32px;
  887. box-shadow: rgba(0, 0, 0, 0.25) 0 0 25px;
  888. padding: 0;
  889. }
  890. .nst-dialog-wrapper {
  891. display: flex;
  892. flex-direction: column;
  893. padding: 24px;
  894. max-height: calc(100vh - 48px);
  895. max-width: calc(100vw - 48px);
  896. }
  897. .nst-content {
  898. flex: 1;
  899. display: flex;
  900. flex-direction: column;
  901. overflow: auto;
  902. }
  903. .nst-dialog .preview-marks-wrapper {
  904. display: flex;
  905. flex-wrap: wrap;
  906. justify-content: center;
  907. padding-top: 16px;
  908. gap: 8px;
  909. }
  910. .nst-dialog * {
  911. font-family: 'Nunito';
  912. color: var(--nst-text-secondary);
  913. }
  914. .nst-dialog .nst-headline:first-child {
  915. margin-top: 0px;
  916. }
  917. .nst-dialog .nst-headline {
  918. color: var(--nst-text-primary);
  919. font-size: 1.5em;
  920. margin: 0px;
  921. margin-bottom: 16px;
  922. }
  923. .nst-dialog .nst-actions {
  924. margin-top: 16px;
  925. display: flex;
  926. gap: 8px;
  927. justify-content: flex-end;
  928. }
  929. .nst-dialog button {
  930. cursor: pointer;
  931. color: var(--button-color);
  932. border-radius: 28px;
  933. padding: 12px;
  934. margin: 0;
  935. border: none;
  936. outline: none;
  937. transition: 0.2s;
  938. background: var(--nst-quaternary);
  939. }
  940. .nst-dialog button:not([disabled]):hover {
  941. background: var(--nst-tertiary);
  942. }
  943. .nst-dialog button:not([disabled]):active {
  944. background: var(--nst-secondary);
  945. }
  946. .nst-dialog button[disabled] {
  947. opacity: 0.5;
  948. cursor: default;
  949. }
  950. .nst-dialog input {
  951. text-shadow: none;
  952. box-shadow: none;
  953. line-height: normal;
  954. border: 2px solid var(--nst-tertiary);
  955. color: var(--nst-text-secondary);
  956. border-radius: 16px;
  957. padding: 12px;
  958. background: var(--nst-quaternary);
  959. transition: 0.2s;
  960. }
  961. .nst-dialog :not(.preview-marks-wrapper) > input {
  962. width: 100%;
  963. min-width: 100px;
  964. }
  965. .nst-dialog input::-webkit-outer-spin-button,
  966. .nst-dialog input::-webkit-inner-spin-button {
  967. -webkit-appearance: none;
  968. margin: 0;
  969. }
  970. .nst-dialog input[disabled] {
  971. border: 2px solid var(--nst-tertiary);
  972. opacity: 0.5;
  973. }
  974. .nst-dialog input[disabled]:hover,
  975. .nst-dialog input[disabled]:focus,
  976. .nst-dialog input[disabled]:active {
  977. border: 2px solid transparent;
  978. border-radius: 16px;
  979. color: var(--nst-text-secondary);
  980. box-shadow: none;
  981. padding: 12px;
  982. }
  983. .nst-dialog input:hover {
  984. border: 2px solid transparent;
  985. color: var(--nst-text-secondary);
  986. box-shadow: none;
  987. }
  988. .nst-dialog input:focus,
  989. .nst-dialog input:active {
  990. border: 2px solid transparent;
  991. color: var(--nst-text-secondary);
  992. background: var(--nst-tertiary);
  993. box-shadow: none;
  994. }
  995. .nst-dialog .nst-radio-wrapper {
  996. padding: 4px;
  997. }
  998. .nst-dialog input[type="radio"] {
  999. display: none;
  1000. }
  1001. .nst-dialog input[type="radio"] + label {
  1002. padding-left: 20px;
  1003. }
  1004. .nst-dialog input[type="radio"] + label:before {
  1005. content: "";
  1006. display: inline-block;
  1007. position: absolute;
  1008. margin: 2px;
  1009. left: 22px;
  1010. width: 16px;
  1011. height: 16px;
  1012. border-radius: 50%;
  1013. border: 2px solid var(--nst-tertiary);
  1014. background: var(--nst-quaternary);
  1015. }
  1016. .nst-dialog input[type="radio"]:checked + label:before {
  1017. background-color: var(--nst-primary);
  1018. border-color: var(--nst-primary);
  1019. }
  1020. .nst-dialog .nst-area {
  1021. border-radius: 16px;
  1022. background: var(--nst-quaternary);
  1023. padding: 16px;
  1024. width: 100%;
  1025. box-sizing: border-box;
  1026. }
  1027. .nst-dialog .nst-switch {
  1028. position: relative;
  1029. display: inline-block;
  1030. width: 3.5em;
  1031. height: 2em;
  1032. margin: 0;
  1033. }
  1034. .nst-dialog .nst-switch .nst-hide {
  1035. opacity: 0;
  1036. width: 0;
  1037. height: 0;
  1038. }
  1039. .nst-dialog .nst-switch div {
  1040. position: absolute;
  1041. cursor: pointer;
  1042. top: 0;
  1043. left: 0;
  1044. right: 0;
  1045. bottom: 0;
  1046. background: var(--nst-quaternary);
  1047. border: 2px solid var(--nst-tertiary);
  1048. border-radius: 24px;
  1049. transition: .4s;
  1050. }
  1051. .nst-dialog .nst-switch div:before {
  1052. position: absolute;
  1053. content: "";
  1054. height: 1.2em;
  1055. width: 1.2em;
  1056. left: calc(0.4em - 2px);
  1057. top: calc(0.4em - 2px);
  1058. background: var(--nst-tertiary);
  1059. border-radius: 50%;
  1060. transition: .4s;
  1061. }
  1062. .nst-dialog .nst-switch input:checked + div {
  1063. background: var(--nst-primary);
  1064. border: 2px solid transparent;
  1065. }
  1066. .nst-dialog .nst-switch input:checked + div:before {
  1067. transform: translateX(1.4em);
  1068. background: rgba(0, 0, 0, 0.5);
  1069. }
  1070. .nst-dialog table {
  1071. margin-left: auto;
  1072. margin-right: auto;
  1073. }
  1074. .nst-dialog td {
  1075. padding: 8px;
  1076. position: relative;
  1077. }
  1078. .nst-total-cell {
  1079. right: 0;
  1080. position: sticky !important;
  1081. cursor: pointer;
  1082. background: var(--nst-quinary);
  1083. transition: 0.1s;
  1084. }
  1085. .nst-dialog tr {
  1086. transition: 0.2s;
  1087. }
  1088. .nst-dialog tr.nst-row-selected {
  1089. background: var(--nst-quaternary);
  1090. }
  1091. tr.nst-row-selected .nst-total-cell:last-child {
  1092. background: inherit;
  1093. }
  1094. .nst-dialog .nst-marks-table-wrapper {
  1095. overflow: auto;
  1096. margin-left: auto;
  1097. margin-right: auto;
  1098. border-radius: 24px;
  1099. max-width: 100%;
  1100. }
  1101. .nst-dialog .nst-marks-table-wrapper table {
  1102. margin-left: initial;
  1103. margin-right: initial;
  1104. }
  1105. .nst-marks-cell {
  1106. display: flex;
  1107. }
  1108. .nst-controls {
  1109. display: flex;
  1110. flex-wrap: wrap;
  1111. justify-content: space-evenly;
  1112. gap: 8px;
  1113. padding-top: 8px;
  1114. }
  1115. .nst-controls button {
  1116. flex-grow: 1;
  1117. }
  1118. .nst-dialog[open] {
  1119. animation: nst-show-dialog 0.5s forwards;
  1120. }
  1121. .nst-dialog.nst-hide-dialog {
  1122. animation: nst-hide-dialog 0.5s forwards;
  1123. }
  1124. @keyframes nst-show-dialog {
  1125. from {
  1126. opacity: 0;
  1127. transform: scale(0.5);
  1128. }
  1129. to {
  1130. opacity: 1;
  1131. transform: scale(1);
  1132. }
  1133. }
  1134. @keyframes nst-hide-dialog {
  1135. to {
  1136. opacity: 0;
  1137. transform: scale(0.5);
  1138. }
  1139. }
  1140. .nst-dialog::backdrop {
  1141. background: rgba(0, 0, 0, 0.5);
  1142. backdrop-filter: blur(5px);
  1143. animation: none;
  1144. }
  1145. .nst-dialog[open]::backdrop {
  1146. animation: nst-show-opacity 0.5s forwards;
  1147. }
  1148. .nst-dialog.nst-hide-dialog::backdrop {
  1149. animation: nst-hide-opacity 0.5s forwards;
  1150. }
  1151. @keyframes nst-show-opacity {
  1152. from {
  1153. opacity: 0;
  1154. }
  1155. to {
  1156. opacity: 1;
  1157. }
  1158. }
  1159. @keyframes nst-hide-opacity {
  1160. to {
  1161. opacity: 0;
  1162. }
  1163. }
  1164. .nst-label-title {
  1165. font-size: 18px;
  1166. }
  1167. .nst-label-description {
  1168. font-size: 12px;
  1169. color: var(--nst-text-tertiary);
  1170. }
  1171. .nst-mark {
  1172. min-width: 24px;
  1173. display: flex;
  1174. flex-flow: column wrap;
  1175. align-items: stretch;
  1176. cursor: pointer;
  1177. }
  1178. .nst-mark-selectors {
  1179. display: flex;
  1180. gap: 8px;
  1181. padding-top: 8px;
  1182. justify-content: space-between;
  1183. }
  1184. .nst-mark p {
  1185. text-align: center;
  1186. margin: 0px;
  1187. }
  1188. .nst-mark p:first-child {
  1189. font-size: large;
  1190. }
  1191. .nst-mark p:nth-child(2) {
  1192. color: gray;
  1193. font-size: x-small;
  1194. }
  1195. .nst-mark-highlight {
  1196. animation: nst-mark-highlight 3s forwards;
  1197. }
  1198. @keyframes nst-mark-highlight {
  1199. 0% {
  1200. opacity: 1;
  1201. } 16% {
  1202. opacity: 0;
  1203. } 32% {
  1204. opacity: 1;
  1205. } 48% {
  1206. opacity: 0;
  1207. } 64% {
  1208. opacity: 1;
  1209. } 80% {
  1210. opacity: 0;
  1211. } 96% {
  1212. opacity: 1;
  1213. }
  1214. }
  1215. .nst-mark-excellent {
  1216. color: #96e400;
  1217. }
  1218. .nst-mark-good {
  1219. color: #00c8ff;
  1220. }
  1221. .nst-mark-average {
  1222. color: #f09600;
  1223. }
  1224. .nst-mark-bad {
  1225. color: #ff3232;
  1226. }
  1227. .nst-dialog ::-webkit-scrollbar {
  1228. width: 16px;
  1229. height: 16px;
  1230. }
  1231. .nst-dialog ::-webkit-scrollbar-track {
  1232. background: var(--nst-senary);
  1233. border-radius: 10px;
  1234. }
  1235. .nst-dialog ::-webkit-scrollbar-corner {
  1236. background: transparent;
  1237. }
  1238. .nst-dialog ::-webkit-scrollbar-thumb {
  1239. background-color: var(--nst-quaternary);
  1240. border: 4px solid var(--nst-senary);
  1241. border-radius: 10px;
  1242. }
  1243. .nst-dialog ::-webkit-scrollbar-thumb:hover {
  1244. background-color: var(--nst-tertiary);
  1245. }
  1246. .nst-dialog ::-webkit-scrollbar-thumb:active {
  1247. background-color: var(--nst-secondary);
  1248. }
  1249. `)