Greasy Fork is available in English.

Google Classroom | Interface modification

Modification of the interface for the 𝗚𝗼𝗼𝗴𝗹𝗲 𝗖𝗹𝗮𝘀𝘀𝗿𝗼𝗼𝗺.

  1. // ==UserScript==
  2. // @name Google Classroom | Interface modification
  3. // @description Modification of the interface for the 𝗚𝗼𝗼𝗴𝗹𝗲 𝗖𝗹𝗮𝘀𝘀𝗿𝗼𝗼𝗺.
  4.  
  5. // @name:en Google Classroom | Interface modification
  6. // @description:en Modification of the interface for the 𝗚𝗼𝗼𝗴𝗹𝗲 𝗖𝗹𝗮𝘀𝘀𝗿𝗼𝗼𝗺.
  7.  
  8. // @name:ru Google Classroom | Модификация интерфейса
  9. // @description:ru Модификация интерфейса для 𝗚𝗼𝗼𝗴𝗹𝗲 𝗖𝗹𝗮𝘀𝘀𝗿𝗼𝗼𝗺.
  10.  
  11. // @name:uk Google Classroom | | Модифікація інтерфейсу
  12. // @description:uk Модифікація інтерфейсу для 𝗚𝗼𝗼𝗴𝗹𝗲 𝗖𝗹𝗮𝘀𝘀𝗿𝗼𝗼𝗺.
  13.  
  14. // @name:bg Google Classroom | | Модификация на интерфейса
  15. // @description:bg Модификация на интерфейса за 𝗚𝗼𝗼𝗴𝗹𝗲 𝗖𝗹𝗮𝘀𝘀𝗿𝗼𝗼𝗺.
  16.  
  17. // @iconURL https://ssl.gstatic.com/classroom/favicon.png
  18. // @version 1.5
  19. // @match https://classroom.google.com/*
  20. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
  21. // @noframes
  22. // @namespace https://stomaks.me
  23. // @supportURL https://stomaks.me?feedback
  24. // @contributionURL https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=stomaks@gmail.com&item_name=Greasy+Fork+donation
  25. // @author Maksim_Stoyanov_(stomaks)
  26. // @developer Maksim_Stoyanov_(stomaks)
  27. // @copyright 2020, Maxim Stoyanov (stomaks.me)
  28. // @license MIT
  29. // @compatible chrome
  30. // @compatible firefox
  31. // @compatible opera
  32. // @compatible safari
  33. // ==/UserScript==
  34.  
  35.  
  36.  
  37. (function() {
  38. 'use strict';
  39.  
  40. // Интеграция иконок
  41. $(`head`).append(`
  42. <link rel="preload" as="font" href="//stomaks.app/fonts/MaterialIcons/MaterialIcons1.woff2" type="font/woff2" crossorigin="anonymous">
  43. <link rel="preload" as="font" href="//stomaks.app/fonts/MaterialIcons/MaterialIcons2.woff2" type="font/woff2" crossorigin="anonymous">
  44. <link href="//stomaks.app/styles/icons.min.css" rel="stylesheet">`);
  45.  
  46. // Интеграция стилей
  47. {
  48. $(`head`).append(`
  49. <style>
  50.  
  51. .tdS5P {
  52. z-index: 900;
  53. }
  54.  
  55. ol.FpfvHe > li .ClSQxf > [expanded] {
  56. border-radius: 100px;
  57. }
  58.  
  59. ol.FpfvHe > li .ClSQxf > [expanded] > i {
  60. transition:
  61. opacity 250ms 0ms cubic-bezier(.4, 0, .2, 1),
  62. transform 250ms 0ms cubic-bezier(.4, 0, .2, 1);
  63. padding: 8px;
  64. border-radius: 100px;
  65. }
  66.  
  67. ol.FpfvHe > li[expanded="false"] .ClSQxf > [expanded] > i {
  68. -webkit-transform: rotate(180deg);
  69. transform: rotate(180deg);
  70. }
  71.  
  72. ol.FpfvHe > li[expanded]:not([expanded="true"]) > .zq2w8b {
  73. display: none;
  74. }
  75.  
  76. .xUYklb {
  77. font-size: 20px;
  78. }
  79.  
  80. .SRX5Hd {
  81. position: fixed;
  82. bottom: 45px;
  83. right: 45px;
  84. height: auto;
  85. width: auto;
  86. z-index: 900;
  87. }
  88.  
  89. .SRX5Hd .aS18D.p0oLxb {
  90. min-width: inherit;
  91. }
  92.  
  93. .SRX5Hd .GcVcmc .RdyDwe {
  94. display: none;
  95. }
  96.  
  97. .SRX5Hd .aS18D.p0oLxb .Fxmcue.cd29Sd {
  98. padding: 20px;
  99. text-align: center;
  100. }
  101.  
  102. .SRX5Hd .aS18D.p0oLxb .Ce1Y1c {
  103. margin: 0;
  104. }
  105.  
  106. .Kb1iQ {
  107. margin: 0 0 0 auto;
  108. }
  109.  
  110. .JPdR6b.e5Emjc.hVNH5c.bzD7fd.qjTEB {
  111. position: fixed !important;
  112. max-height: initial !important;
  113. top: initial !important;
  114. left: initial !important;
  115. right: 25px !important;
  116. bottom: 35px !important;
  117. }
  118.  
  119. .JPdR6b.e5Emjc.hVNH5c.bzD7fd.qjTEB > .XvhY1d {
  120. max-height: initial !important;
  121. }
  122.  
  123. .JPdR6b.e5Emjc.hVNH5c.bzD7fd.qjTEB #stomaks_classroom_automation [icon="open_in_new"] {
  124. opacity: .25;
  125. font-size: 18px;
  126. margin: auto;
  127. position: absolute;
  128. right: 10px;
  129. top: 8px;
  130. z-index: 1;
  131. }
  132.  
  133. main {
  134. padding-bottom: 75px;
  135. }
  136.  
  137. .ClSQxf {
  138. display: flex;
  139. flex-direction: row;
  140. }
  141.  
  142. /* # alt */
  143. body > [name="alt"] {
  144. position: absolute;
  145. top: 50%;
  146. left: 50%;
  147. border-radius: 2px;
  148. padding: 5px 10px;
  149. max-width: 500px;
  150. background: rgba(97, 97, 97, 0.9);
  151. -webkit-transition: opacity 250ms 250ms cubic-bezier(.4, 0, .2, 1),
  152. transform 250ms 250ms cubic-bezier(.4, 0, .2, 1),
  153. top 250ms 0ms cubic-bezier(.4, 0, .2, 1),
  154. left 250ms 0ms cubic-bezier(.4, 0, .2, 1);
  155. transition: opacity 250ms 250ms cubic-bezier(.4, 0, .2, 1),
  156. transform 250ms 250ms cubic-bezier(.4, 0, .2, 1),
  157. top 250ms 0ms cubic-bezier(.4, 0, .2, 1),
  158. left 250ms 0ms cubic-bezier(.4, 0, .2, 1);
  159. -webkit-transform: scale3d(0, 0, 0);
  160. transform: scale3d(0, 0, 0);
  161. -webkit-transform-origin: top left;
  162. transform-origin: top left;
  163. opacity: 0;
  164. pointer-events: none;
  165. z-index: 970;
  166. }
  167.  
  168. body > [name="alt"],
  169. body > [name="alt"] > * {
  170. color: #fff;
  171. font-family: Roboto, Helvetica, Arial, sans-serif;
  172. font-size: 10px;
  173. line-height: initial;
  174. letter-spacing: .5px;
  175. }
  176.  
  177. body > [name="alt"][state="show"] {
  178. -webkit-transform: scale3d(1, 1, 1);
  179. transform: scale3d(1, 1, 1);
  180. opacity: 1;
  181. }
  182.  
  183. </style>`);
  184. }
  185.  
  186.  
  187.  
  188.  
  189. // Интеграция кнопки споилера
  190. $(`body`).append(`<div name="alt"></div>`);
  191.  
  192.  
  193.  
  194.  
  195.  
  196. //+----------------------------------------------------------------------------------------------+
  197. // Установка подсказок
  198. function tick () {
  199. // Скрыть темы
  200. {
  201. Object.keys(localStorage).forEach(function ( key, i ) {
  202. const value = window.localStorage.getItem(key);
  203.  
  204. $(`ol.FpfvHe > li[data-dom-id='${key}']`).attr("expanded", value);
  205. $(`ol.FpfvHe > li[data-dom-id='${key}'] .ClSQxf > [expanded]`).attr("expanded", value);
  206. });
  207. }
  208.  
  209. // Для основного меню
  210. {
  211. const el = $(`div[role="menu"].OX4Vcb a[aria-label]`);
  212.  
  213. el.each(function () {
  214. const el_text = $(this).attr("aria-label");
  215.  
  216. $(this).attr("alt", el_text);
  217. });
  218. }
  219.  
  220. // Установка подсказок - Для меню
  221. {
  222. const el = $(`aside.GP1o5c ul > li`);
  223.  
  224. el.each(function () {
  225. const el_text = $(this).find(`div.YVvGBb`).text();
  226.  
  227. $(this).attr("alt", el_text);
  228. });
  229. }
  230.  
  231. // Установка подсказок - Для тем
  232. {
  233. const el = $(`ol.FpfvHe > li > div[data-topic-id]`);
  234.  
  235. el.each(function () {
  236. const el_text = $(this).find(`> div > a`).text();
  237.  
  238. $(this).attr("alt", el_text);
  239. });
  240. }
  241.  
  242. // Установка подсказок - Для елементов темы
  243. {
  244. const el = $(`ol.Xzp3fc > li > div`);
  245.  
  246. el.each(function () {
  247. const el_text = $(this).find(`.kByKEb > span`).text();
  248.  
  249. $(this).attr("alt", el_text);
  250. });
  251. }
  252.  
  253. // Кнопка +
  254. {
  255. const el = $(`.SRX5Hd`);
  256.  
  257. const el_text = el.find(`.GcVcmc .RdyDwe`).text();
  258.  
  259. el.attr("alt", el_text);
  260. }
  261.  
  262. // Установка кнопок для споилера
  263. {
  264. const el = $(`.ClSQxf`);
  265.  
  266. if ( !el.find(`[expanded]`).length ) {
  267. el.prepend(`<div class="wwnMtb" expanded="true"><i icon="expand_less"></i></div>`);
  268. }
  269. }
  270.  
  271. // Добавить опцию создания
  272. {
  273. const el = $(`.JPdR6b.e5Emjc.hVNH5c.bzD7fd.qjTEB > div > div`);
  274.  
  275. if ( !el.find(`#stomaks_classroom_automation`).length ) {
  276. el.append(`
  277. <div role="separator" class="kCtYwe"></div>
  278. <span jsslot id="stomaks_classroom_automation" tabindex="-1" class="z80M1 FeRvI" aria-label="Автоматизация" role="menuitem">
  279. <div class="aBBjbd MbhUzd" jsname="ksKsZd"></div>
  280. <div class="PCdOIb Ce1Y1c" aria-hidden="true">
  281. <i icon="-cogs" class="mxmXhf NMm5M hhikbc"></i>
  282. </div>
  283. <div class="uyYuVb oJeWuf">
  284. <div class="jO7h3c">Автоматизация</div>
  285. </div>
  286. <div>
  287. <i icon="open_in_new"></i>
  288. </div>
  289. </span>`);
  290. }
  291. }
  292.  
  293. setTimeout(tick, 1000);
  294. };
  295.  
  296. setTimeout(tick, 100);
  297. //+----------------------------------------------------------------------------------------------+
  298.  
  299.  
  300.  
  301.  
  302.  
  303. //+----------------------------------------------------------------------------------------------+
  304. /** Метод-утилита "getCoordinates" - Получает координаты курсора или элемента.
  305. *
  306. * @param {string|jQuery} Путь к элементу, ссылка на элемент или объект с настройками.
  307. *
  308. * @param {object} callback Данные для подписанных функций или функция обратного вызова.
  309. *
  310. * @return {object|null|function} Объект, или выполняет функцию обратного вызова.
  311. */
  312. function getCoordinates ( data, callback = null, event = window.event ) {
  313. let result = {};
  314.  
  315. try {
  316. result.data = {};
  317.  
  318. if ( data == null ) {
  319. result.data = {
  320. X: event.clientX || null,
  321. Y: event.clientY || null,
  322. x: event.pageX || null,
  323. y: event.pageY || null
  324. };
  325. } else {
  326. switch ( typeof data ) {
  327. case "string":
  328. data = $(data);
  329.  
  330. case "object":
  331. if ( data instanceof jQuery && data.is(":visible") ) {
  332. result.data = {
  333. X: data.position().left || null,
  334. Y: data.position().top || null,
  335. x: data.offset().left || null,
  336. y: data.offset().top || null
  337. };
  338. }
  339. break;
  340.  
  341. default: break;
  342. }
  343. }
  344.  
  345.  
  346.  
  347. // Без обратного вызова
  348. if ( !callback ) {
  349. return result.data;
  350. }
  351.  
  352. // Функция обратного вызова
  353. if ( typeof callback === "function" ) {
  354. return callback( result );
  355. }
  356. } catch ( error ) {
  357. result.error = error;
  358. result.data = null;
  359. }
  360.  
  361. return result.data;
  362. };
  363. //+----------------------------------------------------------------------------------------------+
  364.  
  365.  
  366.  
  367.  
  368.  
  369. //+----------------------------------------------------------------------------------------------+
  370. /** Метод-действие "showAlt" - Отображает подсказку.
  371. *
  372. * @param {string|jQuery} Путь к элементу, или ссылка на элемент, или объект с настройками.
  373. *
  374. * @param {object} callback Данные для подписанных функций или функция обратного вызова.
  375. *
  376. * @return {object|null|function} Объект, или выполняет функцию обратного вызова.
  377. */
  378. function showAlt ( data, callback = null, event = window.event ) {
  379. let result = {};
  380.  
  381. result.data = {};
  382.  
  383. try {
  384. function _ ( x, y ) {
  385. let float = [];
  386. let temp = x / $(`body`).width() * 100;
  387.  
  388. if ( temp <= 10 ) {
  389. float.push("left");
  390. } else if ( temp > 10 && temp < 90 ) {
  391. float.push("center");
  392. } else {
  393. float.push("right");
  394. }
  395.  
  396. temp = y / $(`body`).height() * 100;
  397.  
  398. if ( temp <= 10 ) {
  399. float.push("top");
  400. } else if ( temp > 10 && temp < 90 ) {
  401. float.push("center");
  402. } else {
  403. float.push("bottom");
  404. }
  405.  
  406. return float;
  407. }
  408.  
  409. // Контейнер
  410. switch ( typeof data ) {
  411. case "string":
  412. if ( data.length > 0 ) {
  413. result.data.container = $(data);
  414. break;
  415. }
  416.  
  417. case "object":
  418. if ( data instanceof jQuery ) {
  419. result.data.container = data;
  420. break;
  421. }
  422.  
  423. default:
  424. throw new TypeError(`Входящие данные не определены или имеют неверный тип данных.`);
  425. }
  426.  
  427. // Подсказка
  428. result.data.alt = result.data.container.attr("alt");
  429.  
  430. // Направление подсказки
  431. result.data.float = [];
  432. {
  433. let temp = result.data.container.attr("alt-float");
  434.  
  435. if ( typeof temp === "string" ) {
  436. temp = temp.split(" ");
  437.  
  438. result.data.float = [temp[0], temp[1]];
  439. }
  440. }
  441.  
  442. // Определение соответствия текста в alt и в элементе
  443. function isAlt ( el, alt ) {
  444. if ( el.children().length > 0 ) {
  445. // Видимые в элементы
  446. el = el.children(`:visible:not(.content)`).filter(function() {
  447. return !($(this).css(`opacity`) === "0" || $(this).css(`visibility`) === "hidden");
  448. });
  449. }
  450.  
  451. // Текст
  452. result.data.text = el.text().replace(/^\s+|\s+$/g, ``);
  453.  
  454. return (typeof alt === "string" && alt.length ); // && alt !== result.data.text
  455. }
  456.  
  457. if ( isAlt(result.data.container, result.data.alt) ) {
  458. $(`body > div[name="alt"]`)
  459. .attr("state", "show")
  460. .html( result.data.alt );
  461.  
  462.  
  463.  
  464. // Получить координаты контейнера
  465. result.data.coordinates = getCoordinates( data );
  466.  
  467. // Валидация направления подсказки
  468. {
  469. let isX = false;
  470. if ( result.data.float[0] === "left" || result.data.float[0] === "center" || result.data.float[0] === "right" ) {
  471. isX = true;
  472. }
  473.  
  474. let isY = false;
  475. if ( result.data.float[1] === "top" || result.data.float[1] === "center" || result.data.float[1] === "bottom" ) {
  476. isY = true;
  477. }
  478.  
  479. if ( !isX || !isY || result.data.float.length !== 2 ) {
  480. // Получить положение контейнера
  481. let temp = _(result.data.coordinates.x, result.data.coordinates.y);
  482.  
  483. if ( !isX ) {
  484. result.data.float = [temp[0], result.data.float[1]];
  485. }
  486.  
  487. if ( !isY ) {
  488. result.data.float = [result.data.float[0], temp[1]];
  489. }
  490. }
  491. }
  492.  
  493.  
  494.  
  495. let app_width = $(`body`).outerWidth() || $(`body`).width();
  496. let app_height = $(`body`).outerHeight() || $(`body`).height();
  497.  
  498. let alt_width = $(`body > div[name="alt"]`).outerWidth() || $(`body > div[name="alt"]`).width();
  499. let alt_height = $(`body > div[name="alt"]`).outerHeight() || $(`body > div[name="alt"]`).height();
  500.  
  501. let container_width = result.data.container.outerWidth() || result.data.container.width();
  502. let container_height = result.data.container.outerHeight() || result.data.container.height();
  503.  
  504. switch ( result.data.float.join(" ") ) {
  505. case "left top": // ↘
  506. result.data.coordinates.y += container_height + 10;
  507. break;
  508.  
  509. case "left center": // →
  510. result.data.coordinates.x += container_width + 20;
  511. result.data.coordinates.y += (container_height - alt_height ) / 2;
  512. break;
  513.  
  514. case "left bottom": // ↗
  515. result.data.coordinates.x += container_width + 20;
  516. result.data.coordinates.y += (container_height - alt_height ) / 2;
  517. break;
  518.  
  519. case "center top": // ↓
  520. result.data.coordinates.x += ((container_width - alt_width) / 2);
  521. result.data.coordinates.y += container_height + 10;
  522. break;
  523.  
  524. case "center center": // •
  525. result.data.coordinates.x += ((container_width - alt_width) / 2);
  526. result.data.coordinates.y += container_height + 10;
  527. break;
  528.  
  529. case "center bottom": // ↑
  530. result.data.coordinates.x += ((container_width - alt_width) / 2);
  531. result.data.coordinates.y -= alt_height + 10;
  532. break;
  533.  
  534. case "right top": // ↙
  535. result.data.coordinates.x += container_width - alt_width;
  536. result.data.coordinates.y += container_height + 10;
  537. break;
  538.  
  539. case "right center": // ←
  540. result.data.coordinates.x -= alt_width + 25;
  541. result.data.coordinates.y += (container_height - alt_height ) / 2;
  542. break;
  543.  
  544. case "right bottom": // ↖
  545. result.data.coordinates.x -= alt_width + 25;
  546. result.data.coordinates.y += (container_height - alt_height ) / 2;
  547. break;
  548. }
  549.  
  550. if ( result.data.coordinates.x < 20 ) {
  551. result.data.coordinates.x = 20;
  552. }
  553. if ( result.data.coordinates.x + 40 >= app_width ) {
  554. result.data.coordinates.x = app_width - (alt_width + 20);
  555. }
  556.  
  557. if ( result.data.coordinates.y < 20 ) {
  558. result.data.coordinates.y = 20;
  559. }
  560. if ( result.data.coordinates.y + 40 >= app_height ) {
  561. result.data.coordinates.y = app_height - (alt_height + 20);
  562. }
  563.  
  564.  
  565.  
  566. $(`body > div[name="alt"]`)
  567. .css({
  568. "left": result.data.coordinates.x,
  569. "top": result.data.coordinates.y,
  570. "-webkit-transform-origin": result.data.float.join(" "),
  571. "transform-origin": result.data.float.join(" ")
  572. });
  573. } else {
  574. $(`body > div[name="alt"]`)
  575. .attr("state", "hide");
  576. }
  577.  
  578.  
  579.  
  580. // Без обратного вызова
  581. if ( !callback ) {
  582. return result.data;
  583. }
  584.  
  585. // Функция обратного вызова
  586. if ( typeof callback === "function" ) {
  587. return callback( result );
  588. }
  589. } catch ( error ) {
  590. result.error = error;
  591. result.data = null;
  592. }
  593.  
  594. return result.data;
  595. };
  596. //+----------------------------------------------------------------------------------------------+
  597.  
  598.  
  599.  
  600.  
  601.  
  602. $(`html > body`)
  603.  
  604. // Показать подсказку
  605. .on("mouseover focus", "*", function ( event = window.event ) {
  606. // Контейнер
  607. let el_container = $(this).find(event.target).closest(`[alt]`);
  608.  
  609. showAlt( el_container );
  610. })
  611.  
  612. // [Свернуть|Развернуть споилер]
  613. .on("mouseup", ".ClSQxf > [expanded]", function ( event = window.event ) {
  614. const el = $(this);
  615. const el_container = el.closest(`li`);
  616.  
  617. let expanded = el.attr("expanded");
  618. const id = el_container.attr("data-dom-id");
  619.  
  620. if ( expanded == "true" ) {
  621. expanded = "false";
  622. } else {
  623. expanded = "true";
  624. }
  625.  
  626. el.attr("expanded", expanded);
  627. el_container.attr("expanded", expanded);
  628.  
  629. localStorage.setItem(id, expanded);
  630. console.log( id, expanded );
  631. })
  632.  
  633. // Показать подсказку
  634. .on("mouseover focus", "#stomaks_classroom_automation", function ( event = window.event ) {
  635. const el = $(this);
  636.  
  637. el.addClass("FwR7Pc");
  638. })
  639.  
  640. // Кнопка "Автоматизация"
  641. .on("mouseover focus", "#stomaks_classroom_automation", function ( event = window.event ) {
  642. const el = $(this);
  643.  
  644. el.addClass("FwR7Pc");
  645. })
  646. .on("mouseleave focusout", "#stomaks_classroom_automation", function ( event = window.event ) {
  647. const el = $(this);
  648.  
  649. el.removeClass("FwR7Pc");
  650. })
  651. .on("mouseup", "#stomaks_classroom_automation", function ( event = window.event ) {
  652. window.open("https://g-apps-script.com/blog/google-klass", "_blank");
  653. })
  654. ;
  655.  
  656. })();