Greasy Fork is available in English.

Habr.Features

Всякое-разное для Habr aka habr.com

  1. // ==UserScript==
  2. // @name Habr.Features
  3. // @version 3.7.71
  4. // @description Всякое-разное для Habr aka habr.com
  5. // @author AngReload
  6. // @include https://habr.com/*
  7. // @include http://habr.com/*
  8. // @namespace habr_comments
  9. // @run-at document-start
  10. // @grant GM.xmlHttpRequest
  11. // @connect m.habr.com
  12. // @icon https://habr.com/favicon.ico
  13. // ==/UserScript==
  14. /* global localStorage, MutationObserver, GM */
  15.  
  16. // настройки по умолчанию
  17. const FLAGS = {};
  18.  
  19. // размеры кнопок
  20. FLAGS.SMALL_BUTTONS = false;
  21.  
  22. // Предотвратить закрытие страницы, если статья удалена
  23. FLAGS.PREVENT_CLOSING_DELETED_PAGES = false;
  24.  
  25. // остановка гифок
  26. // клик по гифке заменит картинку на заглушку
  27. // повторный клик вернет гифку на место
  28. FLAGS.GIF_STOP = true;
  29. // остановить гифки при загрузке страницы
  30. FLAGS.GIF_STOP_ONLOAD = false;
  31. // цвета заглушки
  32. FLAGS.GIF_STOP_COLOR_FG = 'White'; // White
  33. FLAGS.GIF_STOP_COLOR_BG = 'LightGray'; // LightGray or WhiteSmoke
  34. // менять src вместо создания-удаления нод
  35. FLAGS.GIF_STOP_OVERTYPE = false;
  36.  
  37. // показывать счетчики рейтинга в виде:
  38. // рейтинг = число_плюсов - число_минусов
  39. FLAGS.RATING_DETAILS = true;
  40. // клик мышкой по рейтингу меняет вид на простой \ детальный
  41. FLAGS.RATING_DETAILS_ONCLICK = false;
  42. // рейтинг = число_голосовавших * (процент_плюсов - процент_минусов)%
  43. FLAGS.RATING_DETAILS_PN = false;
  44.  
  45. // карма = число_голосовавших * (процент_товарищей - процент_неприятелей)%
  46. FLAGS.KARMA_DETAILS = true;
  47.  
  48. // показывать метки времени в текущем часовом поясе
  49. // абсолютно, либо относительно текущего времени, либо относительно родительского времени
  50. // меняется по клику, в всплывающей подсказке другие виды времени, автообновляется
  51. FLAGS.TIME_DETAILS = true;
  52.  
  53. // добавить возможность сортировки комментариев
  54. FLAGS.COMMENTS_SORT = true;
  55. // сортировать комменты при загрузке страницы или оставить сортировку по времени
  56. FLAGS.COMMENTS_SORT_ONLOAD = true;
  57. // список доступных сортировок
  58. FLAGS.COMMENTS_SORT_BY_FRESHNESS = true;
  59. FLAGS.COMMENTS_SORT_BY_TREND = true; // самая полезная сортировка
  60. FLAGS.COMMENTS_SORT_BY_QUALITY = false;
  61. FLAGS.COMMENTS_SORT_BY_RATING = false;
  62. FLAGS.COMMENTS_SORT_BY_POPULARITY = false;
  63. FLAGS.COMMENTS_SORT_BY_REDDIT = false;
  64. FLAGS.COMMENTS_SORT_BY_RANDOM = false;
  65.  
  66. // добавить возможность сворачивать комментарии
  67. FLAGS.COMMENTS_HIDE = false;
  68. // свернуть комментарии если их глубина вложенности равна некому числу
  69. FLAGS.HIDE_LEVEL = 4;
  70. // свернуть комменты с 4-го уровня
  71. FLAGS.HIDE_LEVEL_4 = false;
  72. // сделать «возврат каретки» для комментариев чтобы глубина вложенности не превышала некого числа
  73. FLAGS.LINE_LEN = 8;
  74. // отбивать каждый следущий уровень вложенности дополнительным отступом
  75. FLAGS.REDUCE_NEW_LINES = true;
  76. // глубина вложенности при «возврате каретки» комментариев обозначается разным цветом
  77. FLAGS.RAINBOW_NEW_LINE = true;
  78.  
  79. // заменить ссылки ведущие к новым комментариям на дерево комментариев
  80. FLAGS.COMMENTS_LINKS = true;
  81.  
  82. // запоминание галки «Использовать MarkDown» для комментариев
  83. FLAGS.COMMENTS_MD = true;
  84.  
  85. // добавить ссылку на хабрасторадж в форму редактирования комментариев
  86. FLAGS.HTSO_BTN = true;
  87.  
  88. // добавить ссылку на отслеживаемых в странице профиля
  89. FLAGS.SUBS_BTN = true;
  90.  
  91. // включить поиск комментариев в профилях
  92. FLAGS.FIND_COMMENTS = true;
  93.  
  94. // линии вдоль прокрутки для отображения размеров поста и комментариев
  95. FLAGS.SCROLL_LEGEND = true;
  96.  
  97. // добавляет кнопку активации ночного режима
  98. FLAGS.NIGHT_MODE = true;
  99.  
  100. // добавляет диалок настроек
  101. FLAGS.CONFIG_INTERFACE = true;
  102.  
  103. // включить разные стили
  104. FLAGS.USERSTYLE = true;
  105. // лента постов с увеличенными отступами, нижний бар выровнен вправо
  106. FLAGS.USERSTYLE_FEED_DISTANCED = true;
  107. // мелкий фикс для отступа в счетчике ленты
  108. FLAGS.USERSTYLE_COUNTER_NEW_FIX_SPACE = true;
  109. // удаляет правую колонку, там где не жалко
  110. FLAGS.USERSTYLE_REMOVE_SIDEBAR_RIGHT = true;
  111. // аватарки без скруглений
  112. FLAGS.USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS = true;
  113. // большие аватарки в профиле и карточках
  114. FLAGS.USERSTYLE_USERINFO_BIG_AVATARS = true;
  115. // ограничить размер картинок в комментах
  116. FLAGS.USERSTYLE_COMMENTS_IMG_MAXSIZE = 0;
  117. // ограничить размер картинок в комментах
  118. FLAGS.USERSTYLE_COMMENTS_OPACITY = false;
  119. // нормальные стили для комментариев
  120. FLAGS.USERSTYLE_COMMENTS_FIX = true;
  121. // нормальные стили для кода
  122. FLAGS.USERSTYLE_CODE_FIX = true;
  123. // окантовка границ спойлеров
  124. FLAGS.USERSTYLE_SPOILER_BORDERS = true;
  125. // делает глючные плавающие блоки статичными
  126. FLAGS.USERSTYLE_STATIC_STICKY = true;
  127. // показывать язык подсветки блоков кода
  128. FLAGS.USERSTYLE_HLJS_LANG = true;
  129. // показывать язык кода только при наведении
  130. FLAGS.USERSTYLE_HLJS_LANG_HOVER = false;
  131. // свой шрифт для блоков кода
  132. FLAGS.USERSTYLE_CODE_FONT = ''; // PT Mono
  133. // размер табов в коде
  134. FLAGS.USERSTYLE_CODE_TABSIZE = 2;
  135. // для ночного режима
  136. FLAGS.USERSTYLE_CODE_NIGHT = true;
  137. // для настроек
  138. FLAGS.USERSTYLE_CONFIG_INTERFACE = true;
  139. FLAGS.SCROLL_BEHAVIOR_SMOOTH = true;
  140.  
  141. const configOptions = [
  142. ['KARMA_DETAILS', 'счётчики кармы с плюсами и минусами'],
  143. ['RATING_DETAILS', 'рейтинги с плюсами и минусами'],
  144. ['RATING_DETAILS_PN', 'рейтинги в процентах'],
  145. ['TIME_DETAILS', 'отметки о времени с подробностями'],
  146. ['PREVENT_CLOSING_DELETED_PAGES', 'предотвратить закрытие страницы, если статья удалена'],
  147. ['GIF_STOP', 'остановка гифок'],
  148. ['GIF_STOP_ONLOAD', 'остановить гифки при загрузке страницы'],
  149. ['COMMENTS_SORT', 'сортировка комментов'],
  150. ['COMMENTS_SORT_ONLOAD', 'сортировать комменты при загрузке'],
  151. ['COMMENTS_SORT_BY_FRESHNESS', 'сортировка новые'],
  152. ['COMMENTS_SORT_BY_TREND', 'сортировка горячие'],
  153. ['COMMENTS_SORT_BY_QUALITY', 'сортировка хорошие'],
  154. ['COMMENTS_SORT_BY_REDDIT', 'сортировка проверенные'],
  155. ['COMMENTS_SORT_BY_RATING', 'сортировка рейтинговые'],
  156. ['COMMENTS_SORT_BY_POPULARITY', 'сортировка популярные'],
  157. ['COMMENTS_SORT_BY_RANDOM', 'сортировка случайные'],
  158. // ['COMMENTS_HIDE', 'сворачивание комментов'],
  159. // ['HIDE_LEVEL_4', 'свернуть комменты с 4-го уровня'],
  160. // ['REDUCE_NEW_LINES', 'возврат каретки с уменьшением ширины'],
  161. // ['RAINBOW_NEW_LINE', 'возврат каретки комментов в разных цветах'],
  162. // ['COMMENTS_LINKS', '"читать комментарии" ведёт на корень комментов'],
  163. // ['COMMENTS_MD', 'запоминать галку MarkDown'],
  164. ['USERSTYLE_COMMENTS_OPACITY', 'заминусованные комменты без прозрачности'],
  165. // ['FIX_JUMPING_SCROLL', 'заморозить высоту статьи при загрузке'],
  166. ['SCROLL_LEGEND', 'ленгенда страницы у скроллбара'],
  167. ['SUBS_BTN', 'табы подписок в профилях'],
  168. ['FIND_COMMENTS', 'поиск по комментариям в профилях'],
  169. ['NIGHT_MODE', 'ночной режим'],
  170. ['SMALL_BUTTONS', 'маленькие кнопки настроек и ночного режима'],
  171. // ['USERSTYLE', 'стилизация'],
  172. ['USERSTYLE_COMMENTS_FIX', 'стили комментов'],
  173. ['USERSTYLE_HLJS_LANG', 'показать язык для блоков кода'],
  174. // ['USERSTYLE_HLJS_LANG_HOVER', 'язык кода скрыт до наведении курсора'],
  175. ['USERSTYLE_STATIC_STICKY', 'зафиксировать плавающие блоки'],
  176. // ['USERSTYLE_SPOILER_BORDERS', 'видимые границы спойлеров'],
  177. ['USERSTYLE_FEED_DISTANCED', 'большие отступы между постами в ленте'],
  178. ['USERSTYLE_REMOVE_SIDEBAR_RIGHT', 'удалить правую колонку сайта'],
  179. // ['USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS', 'квадратные аватарки'],
  180. ['USERSTYLE_USERINFO_BIG_AVATARS', 'по возможности большие аватарки в профилях'],
  181. ];
  182.  
  183. // сохраняем умолчания для панели настроек
  184. if (!localStorage.getItem('habrafixFlags')) {
  185. localStorage.setItem('habrafixFlags', JSON.stringify(FLAGS));
  186. } else {
  187. const jsonString = localStorage.getItem('habrafixFlags');
  188. const loadedConfig = jsonString ? JSON.parse(jsonString) : {};
  189. const loadedKeys = Object.keys(loadedConfig);
  190. Object.keys(FLAGS).forEach((key) => {
  191. if (
  192. loadedKeys.includes(key) &&
  193. configOptions.find(arr => arr[0] === key)
  194. ) {
  195. FLAGS[key] = loadedConfig[key];
  196. }
  197. });
  198. }
  199.  
  200. let BUTTON_SIZE = 16;
  201. let BUTTON_SIZE2 = 25;
  202. let BUTTON_SIZE4 = 48;
  203. let KARMA_WIDTH = 84; // ?
  204.  
  205. if (FLAGS.SMALL_BUTTONS) {
  206. BUTTON_SIZE = 16;
  207. BUTTON_SIZE2 = 25;
  208. BUTTON_SIZE4 = 48;
  209. KARMA_WIDTH = 84; // ?
  210. } else {
  211. BUTTON_SIZE = 32;
  212. BUTTON_SIZE2 = 25;
  213. BUTTON_SIZE4 = 88;
  214. KARMA_WIDTH = 84;
  215. }
  216.  
  217. // интерфейс для хранения настроек
  218. const userConfig = {
  219. // имя записи в localsorage
  220. key: 'habrafix',
  221. // модель настроек: ключ - возможные значения
  222. model: {
  223. time_publications: ['fromNow', 'absolute'],
  224. time_comments: ['fromParent', 'fromNow', 'absolute'],
  225. comments_order: ['trend', 'time'],
  226. scores_details: [true, false],
  227. comment_markdown: [false, true],
  228. night_mode: [false, true],
  229. },
  230. config: {},
  231. // при старте для конфига берем сохраненные параметры либо по умолчанию
  232. init() {
  233. let jsonString = localStorage.getItem(userConfig.key);
  234. const loadedConfig = jsonString ? JSON.parse(jsonString) : {};
  235. const loadedKeys = Object.keys(loadedConfig);
  236. const config = {};
  237. Object.keys(userConfig.model).forEach((key) => {
  238. const exist = loadedKeys.indexOf(key) >= 0;
  239. config[key] = exist ? loadedConfig[key] : userConfig.model[key][0];
  240. });
  241. jsonString = JSON.stringify(config);
  242. localStorage.setItem(userConfig.key, jsonString);
  243. userConfig.config = config;
  244. },
  245. getItem(key) {
  246. const jsonString = localStorage.getItem(userConfig.key);
  247. const config = JSON.parse(jsonString);
  248. return config[key];
  249. // return userConfig.config[key];
  250. },
  251. setItem(key, value) {
  252. let jsonString = localStorage.getItem(userConfig.key);
  253. const config = JSON.parse(jsonString);
  254. config[key] = value;
  255. jsonString = JSON.stringify(config);
  256. localStorage.setItem(userConfig.key, jsonString);
  257. userConfig.config = config;
  258. },
  259. // каруселит параметр по значения модели
  260. shiftItem(key) {
  261. const currentValue = userConfig.getItem(key);
  262. const availableValues = userConfig.model[key];
  263. const currentIdx = availableValues.indexOf(currentValue);
  264. const nextIdx = (currentIdx + 1) % availableValues.length;
  265. const nextValue = availableValues[nextIdx];
  266. userConfig.setItem(key, nextValue);
  267. return nextValue;
  268. },
  269. };
  270. userConfig.init();
  271.  
  272. // свои стили
  273. const userStyleEl = document.createElement('style');
  274. let userStyle = '';
  275.  
  276. if (FLAGS.SCROLL_LEGEND) {
  277. userStyle += `
  278. .legend_el {
  279. position: fixed;
  280. width: 4px;
  281. right: 0;
  282. transition: top 1s ease-out, height 1s ease-out;
  283. z-index: 10000;
  284. }
  285.  
  286. #xpanel {
  287. right: 4px;
  288. }
  289. `;
  290. }
  291.  
  292. if (FLAGS.USERSTYLE_FEED_DISTANCED) {
  293. userStyle += `
  294. .post__body_crop {
  295. text-align: right;
  296. }
  297.  
  298. .post__body_crop .post__text {
  299. text-align: left;
  300. }
  301.  
  302. .post__footer {
  303. text-align: right;
  304. }
  305.  
  306. .posts_list .content-list__item_post {
  307. padding: 40px 0;
  308. }
  309. `;
  310. }
  311.  
  312. if (FLAGS.USERSTYLE_COUNTER_NEW_FIX_SPACE) {
  313. userStyle += `
  314. .toggle-menu__item-counter_new {
  315. margin-left: 4px;
  316. }
  317. `;
  318. }
  319.  
  320. if (FLAGS.USERSTYLE_COMMENTS_OPACITY) {
  321. userStyle += `
  322. .comment__message_downgrade {
  323. opacity: 1;
  324. }
  325. `;
  326. }
  327.  
  328. if (FLAGS.USERSTYLE_REMOVE_SIDEBAR_RIGHT || FLAGS.PREVENT_CLOSING_DELETED_PAGES) {
  329. // remove for
  330. // https://habr.com/post/352896/
  331. // https://habr.com/sandbox/
  332. // https://habr.com/sandbox/115216/
  333. // https://habr.com/users/saggid/posts/
  334. // https://habr.com/users/saggid/comments/
  335. // https://habr.com/users/saggid/favorites/
  336. // https://habr.com/users/saggid/favorites/posts/
  337. // https://habr.com/users/saggid/favorites/comments/
  338. // https://habr.com/company/pvs-studio/blog/353640/
  339. // https://habr.com/company/pvs-studio/blog/
  340. // https://habr.com/company/pvs-studio/blog/top/
  341. // https://habr.com/company/pvs-studio/
  342. // https://habr.com/feed/
  343. // https://habr.com/top/
  344. // https://habr.com/top/yearly/
  345. // https://habr.com/all/
  346. // https://habr.com/all/top10/
  347.  
  348. // display for
  349. // https://habr.com/company/pvs-studio/profile/
  350. // https://habr.com/company/pvs-studio/vacancies/
  351. // https://habr.com/company/pvs-studio/fans/all/rating/
  352. // https://habr.com/company/pvs-studio/workers/new/rating/
  353. // https://habr.com/feed/settings/
  354. // https://habr.com/users/
  355. // https://habr.com/hubs/
  356. // https://habr.com/hubs/admin/
  357. // https://habr.com/companies/
  358. // https://habr.com/companies/category/software/
  359. // https://habr.com/companies/new/
  360. // https://habr.com/flows/design/
  361.  
  362. const path = window.location.pathname;
  363. const isPost = /^\/(ru|en)\/post\/\d+\/$/.test(path);
  364. const isSandbox = /^\/(ru|en)\/sandbox\//.test(path);
  365. const isUserPosts = /^\/(ru|en)\/users\/[^/]+\/posts\//.test(path);
  366. const isUserComments = /^\/(ru|en)\/users\/[^/]+\/comments\//.test(path);
  367. const isUserFavorites = /^\/(ru|en)\/users\/[^/]+\/favorites\//.test(path);
  368. // const isUserSubscription = /^\/(ru|en)\/users\/[^/]+\/subscription\//.test(path);
  369. const isCompanyBlog = /^\/(ru|en)\/company\/[^/]+\/blog\//.test(path);
  370. const isCompanyBlog2 = /^\/(ru|en)\/company\/[^/]+\/(page\d+\/)?$/.test(path);
  371. const isCompanyBlogPost = /^\/(ru|en)\/company\/[^/]+\/blog\/[^/]+/.test(path);
  372. const isFeed = /^\/(ru|en)\/feed\//.test(path);
  373. const isHome = /^\/(ru|en)\/$/.test(path);
  374. const isTop = /^\/(ru|en)\/top\//.test(path);
  375. const isAll = /^\/(ru|en)\/all\//.test(path);
  376. const isNews = /^\/(ru|en)\/news\//.test(path);
  377. const isNewsT = /^\/(ru|en)\/news\/t\/\d+\/$/.test(path);
  378.  
  379. if (FLAGS.USERSTYLE_REMOVE_SIDEBAR_RIGHT && (
  380. isPost || isSandbox ||
  381. isUserPosts || isUserComments || isUserFavorites ||
  382. isCompanyBlog || isCompanyBlog2 ||
  383. isFeed || isHome || isTop || isAll ||
  384. isNews || isNewsT
  385. )) {
  386. userStyle += `
  387. .sidebar_right,
  388. .sidebar {
  389. display: none;
  390. }
  391.  
  392. .content_left {
  393. padding-right: 0;
  394. }
  395.  
  396. .comment_plain {
  397. max-width: 860px;
  398. }
  399. `;
  400. }
  401.  
  402. if (FLAGS.PREVENT_CLOSING_DELETED_PAGES && (
  403. isPost || isSandbox ||
  404. isCompanyBlogPost ||
  405. isNews || isNewsT
  406. )) {
  407. window.onbeforeunload = (e) => {
  408. const xhr = new XMLHttpRequest();
  409. xhr.open('HEAD', '', false);
  410. xhr.send();
  411. if (xhr.status < 200 || xhr.status >= 400) {
  412. e.preventDefault();
  413. const message = 'Статья уже удалена или недоступна';
  414. e.returnValue = message;
  415. return message;
  416. }
  417. return undefined;
  418. };
  419. }
  420. }
  421.  
  422. if (FLAGS.USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS) {
  423. userStyle += `
  424. .user-info__image-pic,
  425. .user-pic_popover,
  426. .media-obj__image-pic,
  427. .company-info__image-pic {
  428. border-radius: 0;
  429. }
  430. `;
  431. }
  432.  
  433. if (FLAGS.USERSTYLE_USERINFO_BIG_AVATARS) {
  434. userStyle += `
  435. .page-header {
  436. height: auto;
  437. }
  438.  
  439. .media-obj__image-pic_hub,
  440. .user-info__stats .media-obj__image-pic_user,
  441. /* .media-obj__image-pic_company, */
  442. .company-info__image-pic {
  443. width: auto;
  444. height: auto;
  445. }
  446.  
  447. .page-header_tall .company-info__image-pic {
  448. width: 48px;
  449. height: 48px;
  450. }
  451. `;
  452. }
  453.  
  454. if (FLAGS.COMMENTS_SORT) {
  455. userStyle += `
  456. .comments_order {
  457. color: #333;
  458. font-size: 14px;
  459. font-family: "-apple-system",BlinkMacSystemFont,Arial,sans-serif;
  460. text-rendering: optimizeLegibility;
  461. border-bottom: 1px solid #e3e3e3;
  462. padding: 8px;
  463. text-align: right;
  464. }
  465.  
  466. .comments_order a {
  467. color: #548eaa;
  468. font-style: normal;
  469. text-decoration: none;
  470. }
  471.  
  472. .comments_order a:hover {
  473. color: #487284;
  474. }
  475. `;
  476. }
  477.  
  478. if (FLAGS.USERSTYLE_COMMENTS_FIX) {
  479. userStyle += `
  480. .content-list_comments {
  481. overflow: visible;
  482. }
  483.  
  484. .comment__folding-dotholder {
  485. display: none !important;
  486. }
  487.  
  488. .content-list_nested-comments {
  489. border-left: 1px solid #e3e3e3;
  490. margin: 0;
  491. /*padding-top: 20px;*/
  492. padding-left: 20px !important;
  493. }
  494.  
  495. .content-list_comments {
  496. /*border-left: 1px solid silver;*/
  497. margin: 0;
  498. /*padding-left: 0;*/
  499. padding-top: 20px;
  500. /*background: #FCE4EC;*/
  501. }
  502.  
  503. #comments-list .js-form_placeholder:not(:empty) {
  504. border-left: 1px solid #e3e3e3;
  505. padding-left: 20px;
  506. }
  507.  
  508. .comments_new-line {
  509. border-left: 1px solid #777;
  510. border-bottom: 1px solid #777;
  511. border-top: 1px solid #777;
  512. margin-left: -${FLAGS.LINE_LEN * 21}px !important;
  513. background: white;
  514. padding-bottom: 4px;
  515. }
  516.  
  517. /* .comment__head_topic-author.comment__head_new-comment */
  518. .comment__head_topic-author .user-info {
  519. text-decoration: underline;
  520. }
  521.  
  522. /* фикс, когда не добавляется класс &_plus или minus при user_vote_action */
  523. .voting-wjt__button[title="Вы проголосовали положительно"] {
  524. color: #7ba600;
  525. }
  526. .voting-wjt__button[title="Вы проголосовали отрицательно"] {
  527. color: #d53c30;
  528. }
  529. `;
  530. }
  531.  
  532. if (FLAGS.USERSTYLE_COMMENTS_FIX && FLAGS.RAINBOW_NEW_LINE) {
  533. userStyle += `
  534. .comments_new-line-1 {
  535. border-color: #0caefb;
  536. }
  537.  
  538. .comments_new-line-2 {
  539. border-color: #06feb7;
  540. }
  541.  
  542. .comments_new-line-3 {
  543. border-color: #fbcb02;
  544. }
  545.  
  546. .comments_new-line-0 {
  547. border-color: #fb0543;
  548. }
  549.  
  550. .js-comment_parent:not(:hover) {
  551. color: #cd66cd !important;
  552. }
  553. `;
  554. }
  555.  
  556. if (FLAGS.USERSTYLE_COMMENTS_FIX && FLAGS.REDUCE_NEW_LINES) {
  557. userStyle += `
  558. .comments_new-line .comments_new-line {
  559. margin-left: -${(FLAGS.LINE_LEN - 1) * 21}px !important;
  560. }
  561. `;
  562. }
  563.  
  564. if (FLAGS.USERSTYLE_COMMENTS_IMG_MAXSIZE) {
  565. userStyle += `
  566. .comment__message img {
  567. max-height: ${FLAGS.USERSTYLE_COMMENTS_IMG_MAXSIZE}px;
  568. }
  569.  
  570. .comment__message .spoiler .img {
  571. max-height: auto;
  572. }
  573. `;
  574. }
  575.  
  576. if (FLAGS.USERSTYLE_CODE_FIX) {
  577. let addFont = '';
  578. if (FLAGS.USERSTYLE_CODE_FONT) {
  579. addFont = FLAGS.USERSTYLE_CODE_FONT;
  580. if (addFont.indexOf(' ') >= 0) {
  581. addFont = `"${addFont}"`;
  582. }
  583. addFont += ',';
  584. }
  585.  
  586. const tabSize = FLAGS.USERSTYLE_CODE_TABSIZE || 4;
  587.  
  588. userStyle += `
  589. .editor .text-holder textarea,
  590. .tm-editor__textarea,
  591. pre {
  592. font-family: ${addFont} 'Ubuntu Mono', Menlo, Monaco, Consolas, 'Lucida Console', 'Courier New', monospace;
  593. }
  594.  
  595. code {
  596. font-family: ${addFont} 'Ubuntu Mono', Menlo, Monaco, Consolas, 'Lucida Console', 'Courier New', monospace !important;;
  597. -o-tab-size: ${tabSize};
  598. -moz-tab-size: ${tabSize};
  599. tab-size: ${tabSize};
  600. background: #f7f7f7;
  601. border-radius: 3px;
  602. color: #505c66;
  603. display: inline-block;
  604. font-weight: 500;
  605. line-height: 1.29;
  606. padding: 5px 9px;
  607. vertical-align: 1px;
  608. }
  609. `;
  610. }
  611.  
  612. if (FLAGS.USERSTYLE_SPOILER_BORDERS) {
  613. userStyle += `
  614. .spoiler .spoiler_text {
  615. border: 1px dashed rgb(12, 174, 251);
  616. }
  617. `;
  618. }
  619.  
  620. if (FLAGS.USERSTYLE_STATIC_STICKY) {
  621. userStyle += `
  622. .wrapper-sticky,
  623. .js-ad_sticky,
  624. .js-ad_sticky_comments {
  625. position: static !important;
  626. }
  627. .sticky-spacer {
  628. display: none !important;
  629. }
  630. `;
  631. }
  632.  
  633. if (FLAGS.GIF_STOP) {
  634. userStyle += `
  635. .habrafix_gif-stop:hover {
  636. outline: 4px solid #548eaa;
  637. outline-offset: -4px;
  638. }
  639. `;
  640. }
  641.  
  642. if (FLAGS.USERSTYLE_HLJS_LANG) {
  643. let hover = '';
  644. if (FLAGS.USERSTYLE_HLJS_LANG_HOVER) hover = ':hover';
  645. userStyle += `
  646. pre {
  647. position: relative;
  648. }
  649.  
  650. .hljs${hover}::after {
  651. position: absolute;
  652. font-size: 12px;
  653. content: 'code';
  654. right: 0;
  655. top: 0;
  656. padding: 1px 5px 0 4px;
  657. /*border-bottom: 1px solid #e5e8ec;
  658. border-left: 1px solid #e5e8ec;
  659. border-bottom-left-radius: 3px;
  660. color: #505c66;*/
  661. opacity: .5;
  662. }
  663. `;
  664. userStyle += [
  665. ['1c', '1C:Enterprise (v7, v8)'],
  666. ['abnf', 'Augmented Backus-Naur Form'],
  667. ['accesslog', 'Access log'],
  668. ['actionscript', 'ActionScript'],
  669. ['ada', 'Ada'],
  670. ['apache', 'Apache'],
  671. ['applescript', 'AppleScript'],
  672. ['arduino', 'Arduino'],
  673. ['armasm', 'ARM Assembly'],
  674. ['asciidoc', 'AsciiDoc'],
  675. ['aspectj', 'AspectJ'],
  676. ['autohotkey', 'AutoHotkey'],
  677. ['autoit', 'AutoIt'],
  678. ['avrasm', 'AVR Assembler'],
  679. ['awk', 'Awk'],
  680. ['axapta', 'Axapta'],
  681. ['bash', 'Bash'],
  682. ['basic', 'Basic'],
  683. ['bnf', 'Backus–Naur Form'],
  684. ['brainfuck', 'Brainfuck'],
  685. ['cal', 'C/AL'],
  686. ['capnproto', 'Cap’n Proto'],
  687. ['ceylon', 'Ceylon'],
  688. ['clean', 'Clean'],
  689. ['clojure-repl', 'Clojure REPL'],
  690. ['clojure', 'Clojure'],
  691. ['cmake', 'CMake'],
  692. ['coffeescript', 'CoffeeScript'],
  693. ['coq', 'Coq'],
  694. ['cos', 'Caché Object Script'],
  695. ['cpp', 'C++'],
  696. ['crmsh', 'crmsh'],
  697. ['crystal', 'Crystal'],
  698. ['cs', 'C#'],
  699. ['csp', 'CSP'],
  700. ['css', 'CSS'],
  701. ['d', 'D'],
  702. ['dart', 'Dart'],
  703. ['delphi', 'Delphi'],
  704. ['diff', 'Diff'],
  705. ['django', 'Django'],
  706. ['dns', 'DNS Zone file'],
  707. ['dockerfile', 'Dockerfile'],
  708. ['dos', 'DOS .bat'],
  709. ['dsconfig', 'dsconfig'],
  710. ['dts', 'Device Tree'],
  711. ['dust', 'Dust'],
  712. ['ebnf', 'Extended Backus-Naur Form'],
  713. ['elixir', 'Elixir'],
  714. ['elm', 'Elm'],
  715. ['erb', 'ERB (Embedded Ruby)'],
  716. ['erlang-repl', 'Erlang REPL'],
  717. ['erlang', 'Erlang'],
  718. ['excel', 'Excel'],
  719. ['fix', 'FIX'],
  720. ['flix', 'Flix'],
  721. ['fortran', 'Fortran'],
  722. ['fsharp', 'F#'],
  723. ['gams', 'GAMS'],
  724. ['gauss', 'GAUSS'],
  725. ['gcode', 'G-code (ISO 6983)'],
  726. ['gherkin', 'Gherkin'],
  727. ['glsl', 'GLSL'],
  728. ['go', 'Go'],
  729. ['golo', 'Golo'],
  730. ['gradle', 'Gradle'],
  731. ['groovy', 'Groovy'],
  732. ['haml', 'Haml'],
  733. ['handlebars', 'Handlebars'],
  734. ['haskell', 'Haskell'],
  735. ['haxe', 'Haxe'],
  736. ['hsp', 'HSP'],
  737. ['htmlbars', 'HTMLBars'],
  738. ['http', 'HTTP'],
  739. ['hy', 'Hy'],
  740. ['inform7', 'Inform 7'],
  741. ['ini', 'Ini'],
  742. ['irpf90', 'IRPF90'],
  743. ['java', 'Java'],
  744. ['javascript', 'JavaScript'],
  745. ['jboss-cli', 'jboss-cli'],
  746. ['json', 'JSON'],
  747. ['julia-repl', 'Julia REPL'],
  748. ['julia', 'Julia'],
  749. ['kotlin', 'Kotlin'],
  750. ['lasso', 'Lasso'],
  751. ['ldif', 'LDIF'],
  752. ['leaf', 'Leaf'],
  753. ['less', 'Less'],
  754. ['lisp', 'Lisp'],
  755. ['livecodeserver', 'LiveCode'],
  756. ['livescript', 'LiveScript'],
  757. ['llvm', 'LLVM IR'],
  758. ['lsl', 'Linden Scripting Language'],
  759. ['lua', 'Lua'],
  760. ['makefile', 'Makefile'],
  761. ['markdown', 'Markdown'],
  762. ['mathematica', 'Mathematica'],
  763. ['matlab', 'Matlab'],
  764. ['maxima', 'Maxima'],
  765. ['mel', 'MEL'],
  766. ['mercury', 'Mercury'],
  767. ['mipsasm', 'MIPS Assembly'],
  768. ['mizar', 'Mizar'],
  769. ['mojolicious', 'Mojolicious'],
  770. ['monkey', 'Monkey'],
  771. ['moonscript', 'MoonScript'],
  772. ['n1ql', 'N1QL'],
  773. ['nginx', 'Nginx'],
  774. ['nimrod', 'Nimrod'],
  775. ['nix', 'Nix'],
  776. ['nsis', 'NSIS'],
  777. ['objectivec', 'Objective-C'],
  778. ['ocaml', 'OCaml'],
  779. ['openscad', 'OpenSCAD'],
  780. ['oxygene', 'Oxygene'],
  781. ['parser3', 'Parser3'],
  782. ['perl', 'Perl'],
  783. ['pf', 'pf'],
  784. ['pgsql', 'PostgreSQL'],
  785. ['plaintext', 'просто текст'], // на будущее
  786. ['php', 'PHP'],
  787. ['pony', 'Pony'],
  788. ['powershell', 'PowerShell'],
  789. ['processing', 'Processing'],
  790. ['profile', 'Python profile'],
  791. ['prolog', 'Prolog'],
  792. ['protobuf', 'Protocol Buffers'],
  793. ['puppet', 'Puppet'],
  794. ['purebasic', 'PureBASIC'],
  795. ['python', 'Python'],
  796. ['q', 'Q'],
  797. ['qml', 'QML'],
  798. ['r', 'R'],
  799. ['rib', 'RenderMan RIB'],
  800. ['roboconf', 'Roboconf'],
  801. ['routeros', 'Microtik RouterOS script'],
  802. ['rsl', 'RenderMan RSL'],
  803. ['ruby', 'Ruby'],
  804. ['ruleslanguage', 'Oracle Rules Language'],
  805. ['rust', 'Rust'],
  806. ['scala', 'Scala'],
  807. ['scheme', 'Scheme'],
  808. ['scilab', 'Scilab'],
  809. ['scss', 'SCSS'],
  810. ['shell', 'Shell Session'],
  811. ['smali', 'Smali'],
  812. ['smalltalk', 'Smalltalk'],
  813. ['sml', 'SML'],
  814. ['sqf', 'SQF'],
  815. ['sql', 'SQL'],
  816. ['stan', 'Stan'],
  817. ['stata', 'Stata'],
  818. ['step21', 'STEP Part 21'],
  819. ['stylus', 'Stylus'],
  820. ['subunit', 'SubUnit'],
  821. ['swift', 'Swift'],
  822. ['taggerscript', 'Tagger Script'],
  823. ['tap', 'Test Anything Protocol'],
  824. ['tcl', 'Tcl'],
  825. ['tex', 'TeX'],
  826. ['thrift', 'Thrift'],
  827. ['tp', 'TP'],
  828. ['twig', 'Twig'],
  829. ['typescript', 'TypeScript'],
  830. ['vala', 'Vala'],
  831. ['vbnet', 'VB.NET'],
  832. ['vbscript-html', 'VBScript in HTML'],
  833. ['vbscript', 'VBScript'],
  834. ['verilog', 'Verilog'],
  835. ['vhdl', 'VHDL'],
  836. ['vim', 'Vim Script'],
  837. ['x86asm', 'Intel x86 Assembly'],
  838. ['xl', 'XL'],
  839. ['xml', 'HTML, XML'],
  840. ['xquery', 'XQuery'],
  841. ['yaml', 'YAML'],
  842. ['zephir', 'Zephir'],
  843. ].map(([langTag, langName]) => `.hljs.${langTag}${hover}::after{content:'${langName} [${langTag}]'}`).join('');
  844. }
  845.  
  846. if (FLAGS.USERSTYLE_CODE_NIGHT) {
  847. userStyle += `
  848. .night_mode_switcher {
  849. box-sizing: border-box;
  850. position: fixed;
  851. width: 32px;
  852. height: 32px;
  853. right: 32px;
  854. bottom: 32px;
  855. width: ${BUTTON_SIZE}px;
  856. height: ${BUTTON_SIZE}px;
  857. right: ${BUTTON_SIZE}px;
  858. bottom: ${BUTTON_SIZE}px;
  859. z-index: 10000;
  860. background-color: transparent;
  861. border-radius: 50%;
  862. border: 4px solid #aaa;
  863. border-right-width: ${BUTTON_SIZE / 2}px;
  864. transition: border-color 0.1s ease-out;
  865. }
  866.  
  867. .night_mode_switcher:hover {
  868. border-color: #333;
  869. }
  870.  
  871. .night .night_mode_switcher {
  872. border-color: #515151;
  873. }
  874.  
  875. .night .night_mode_switcher:hover {
  876. border-color: #9e9e9e;
  877. }
  878.  
  879. .night ::-webkit-scrollbar,
  880. .night ::-webkit-scrollbar-corner,
  881. .night ::-webkit-scrollbar-track-piece {
  882. background-color: #000;
  883. }
  884. .night ::-webkit-scrollbar-thumb {
  885. background-color: #22272b;
  886. border: 1px solid #000;
  887. }
  888. .night ::-webkit-scrollbar-thumb:hover {
  889. background-color: #2C3237;
  890. }
  891. .night {
  892. scrollbar-color: dark;
  893. scrollbar-face-color: #22272b;
  894. scrollbar-track-color: #000;
  895. scrollbar-color: #22272b #000;
  896. }
  897.  
  898. /* bg */
  899. .night .sidebar-block__suggest,
  900. .night .dropdown-container,
  901. .night .poll-result__bar,
  902. .night .comments_new-line,
  903. .night .tm-editor__textarea,
  904. .night .layout,
  905. .night .toggle-menu__most-read,
  906. .night .toggle-menu_most-comments,
  907. .night .partner-info {
  908. background: #171c20;
  909. }
  910.  
  911. /* text */
  912. .night .companies-rating__name:not(:hover),
  913. .night .profile-section__title,
  914. .night .profile-section__about-text,
  915. .night .profile-section__invited,
  916. .night .sidebar-block__suggest,
  917. .night .user-message__body,
  918. .night .promo-block__title_total,
  919. .night .beta-anounce__text,
  920. .night .defination-list__label,
  921. .night .defination-list__value,
  922. .night .search-field__select,
  923. .night .search-field__input[type="text"],
  924. .night .search-form__field,
  925. .night .post-info__title:not(:hover),
  926. .night .dropdown__user-stats,
  927. .night .dropdown-container_white .user-info__special,
  928. .night .n-dropdown-menu__item-link,
  929. .night body,
  930. .night .default-block__polling-title,
  931. .night .poll-result__data-label,
  932. .night code,
  933. .night .user-info__fullname,
  934. .night .user-info__specialization,
  935. .night .page-header__info-title,
  936. .night .page-header__info-desc,
  937. .night .post__title-text,
  938. .night .post__title_link:not(:visited),
  939. .night .checkbox__label,
  940. .night .radio__label,
  941. .night .tm-editor__textarea,
  942. .night .footer-block__title,
  943. .night #TMpanel .container .bmenu > a.current,
  944. .night .post__text-html,
  945. .night .comment__message,
  946. .night .comment-form__preview,
  947. .night .post-share__title,
  948. .night .post-donate__title,
  949. .night .news-block__title,
  950. .night .news-topic__title:not(:visited),
  951. .night .partner-info__title,
  952. .night .partner-info__description {
  953. color: #9e9e9e;
  954. }
  955.  
  956. /* non important text */
  957. .night .icon-svg_bookmark,
  958. .night .icon-svg_views-count,
  959. .night .icon-svg_post-comments,
  960. .night .icon-svg_edit,
  961. .night .icon-svg_recommend,
  962. .night .icon-svg_report,
  963. .night .voting-wjt__button:not(.voting-wjt__button_plus):not(.voting-wjt__button_minus),
  964. .night .icon_comment-edit,
  965. .night .icon_comment-anchor,
  966. .night .icon_comment-bookmark,
  967. .night .icon_comment-branch,
  968. .night .icon_comment-arrow-up,
  969. .night .icon_comment-arrow-down,
  970. .night .layout__elevator {
  971. color: #515151;
  972. }
  973.  
  974. .night .voting-wjt__button:not(.voting-wjt__button_plus):not(.voting-wjt__button_minus):hover,
  975. .night .icon_comment-anchor:hover,
  976. .night .icon_comment-bookmark:hover,
  977. .night .icon_comment-branch:hover,
  978. .night .icon_comment-arrow-up:hover,
  979. .night .icon_comment-arrow-down:hover {
  980. color: #548eaa;
  981. }
  982.  
  983. .night .n-dropdown-menu__item-link:hover {
  984. color: white;
  985. }
  986.  
  987. /* top lvl bg */
  988. .night .h-popover,
  989. .night .profile-section__user-hub:not(.profile-section__user-hub_cross),
  990. .night a.sort-panel__item-toggler.active,
  991. .night .checkbox__label::before,
  992. .night .radio__label::before,
  993. .night .content-list__item_conversation:hover,
  994. .night .search-field__select,
  995. .night .search-field__input[type="text"],
  996. .night .search-form__field,
  997. .night .dropdown-container,
  998. .night .n-dropdown-menu,
  999. .night .post__translatation,
  1000. .night code,
  1001. .night .megapost-teasers,
  1002. .night .tm-editor_comments,
  1003. .night .promo-block__header,
  1004. .night .post__text-html blockquote,
  1005. .night .default-block,
  1006. .night .post-share,
  1007. .night .post-donate,
  1008. .night .company-info__author,
  1009. .night .layout__row_footer-links {
  1010. background: #22272B;
  1011. }
  1012.  
  1013. /* not important bg */
  1014. .night .profile-section__user-hub:not(.profile-section__user-hub_cross):hover,
  1015. .night .btn_blue.disabled,
  1016. .night .btn_blue[disabled],
  1017. .night .tracker_page table.tracker_folowers tr.new,
  1018. .night .dropdown__user-stats,
  1019. .night .comment__head_topic-author,
  1020. .night .promo-item:hover,
  1021. .night .layout__row_navbar,
  1022. .night .layout__row_footer,
  1023. .night #TMpanel,
  1024. .night .n-dropdown-menu__item-link_flow:hover,
  1025. .night #tracker-page .tracker-table__row.new {
  1026. background: #1f2327;
  1027. }
  1028.  
  1029. /* borders */
  1030. .night #tracker-page .tracker-table__header,
  1031. .night #tracker-page .tracker-table__cell,
  1032. .night .h-popover,
  1033. .night .h-popover__stats,
  1034. .night .default-block__footer,
  1035. .night .toggle-menu__item-link_bordered,
  1036. .night .default-block_promote,
  1037. .night .sort-panel,
  1038. .night .n-dropdown-menu_flows,
  1039. .night .for_users_only_msg,
  1040. .night #comments-list .js-form_placeholder,
  1041. .night .sidebar-block__suggest,
  1042. .night .content-list_preview-message,
  1043. .night .btn_outline_blue[disabled],
  1044. .night .user-message__body_html pre code,
  1045. .night .content-list_user-dialog,
  1046. .night .wysiwyg-toolbar,
  1047. .night .content-list__item_bordered,
  1048. .night .promo-block__total,
  1049. .night .search-field__select,
  1050. .night .search-field__input[type="text"],
  1051. .night .search-form__field,
  1052. .night .tracker_page table.tracker_folowers tr td,
  1053. .night .tracker_page table.tracker_folowers tr th,
  1054. .night .stacked-menu__item_devided,
  1055. .night .post__text-html table,
  1056. .night .post__text-html table td,
  1057. .night .post__text-html table th,
  1058. .night .n-dropdown-menu__item_border,
  1059. .night .dropdown-container,
  1060. .night .default-block_bordered,
  1061. .night .default_block_polling,
  1062. .night .column-wrapper_tabs .sidebar_right,
  1063. .night .post__type-label,
  1064. .night .promo-block__header,
  1065. .night .user-info__contacts,
  1066. .night .comment__message pre code,
  1067. .night .comment-form__preview pre code,
  1068. .night .sandbox-panel,
  1069. .night .comment__post-title,
  1070. .night .tm-editor__textarea,
  1071. .night .promo-block__footer,
  1072. .night .author-panel,
  1073. .night .promo-block,
  1074. .night .post__text-html pre code,
  1075. .night .footer-block__title,
  1076. .night #TMpanel,
  1077. .night .layout__row_navbar,
  1078. .night .page-header_bordered,
  1079. .night .post-stats,
  1080. .night .company-info__about,
  1081. .night .company-info_post-additional,
  1082. .night .company-info__contacts,
  1083. .night .post-share,
  1084. .night .post-donate,
  1085. .night .content-list__item_devided,
  1086. .night .comments_order,
  1087. .night .comments-section__head,
  1088. .night .content-list_nested-comments,
  1089. .night .default-block__header,
  1090. .night .column-wrapper_bordered,
  1091. .night .tabs-menu,
  1092. .night .toggle-menu,
  1093. .night .news-block__header,
  1094. .night .news-block__footer {
  1095. border-color: #393d41;
  1096. }
  1097.  
  1098. .night .rating-info__progress,
  1099. .night .poll-result__progress {
  1100. background-color: #515151;
  1101. }
  1102.  
  1103. .night .poll-result__progress_winner {
  1104. background-color: #5e8eac;
  1105. }
  1106.  
  1107. .night .layout__elevator:hover {
  1108. background-color: #22272B;
  1109. }
  1110.  
  1111. .night .comment__head_topic-author {
  1112. background: #003030;
  1113. }
  1114.  
  1115. .night .comment__head_my-comment {
  1116. background: #003000;
  1117. }
  1118.  
  1119. .night .comment__head_new-comment {
  1120. background: black
  1121. }
  1122.  
  1123. .night .user-info__nickname_comment,
  1124. .night .icon-svg_logo-habrahabr {
  1125. color: inherit;
  1126. }
  1127.  
  1128. .night [disabled] {
  1129. opacity: 0.5
  1130. }
  1131.  
  1132. .night .content-list_comments .comment__folding-dotholder::before,
  1133. .night .comment.is_selected::after {
  1134. filter: invert(0.9);
  1135. }
  1136.  
  1137. /* img filter */
  1138. .night .comment__message img,
  1139. .night .comment-form__preview img,
  1140. .night .default-block__content #facebook_like_box,
  1141. .night .default-block__content #vk_groups,
  1142. .night .post img,
  1143. .night .page-header__banner img,
  1144. .night .company_top_banner img,
  1145. .night img .teaser__image,
  1146. .night .teaser__image-pic,
  1147. .night .article__body img {
  1148. filter: brightness(0.5);
  1149. transition: filter .6s ease-out;
  1150. }
  1151.  
  1152. .night .comment__message img:hover,
  1153. .night .comment-form__preview img:hover,
  1154. .night .default-block__content #facebook_like_box:hover,
  1155. .night .default-block__content #vk_groups:hover,
  1156. .night img[alt="en"],
  1157. .night img[alt="habr"],
  1158. .night img:hover,
  1159. .night a.post-author__link img,
  1160. .night img.user-info__image-pic,
  1161. .night .teaser__image-pic:hover,
  1162. .night .teaser__image:hover {
  1163. filter: none;
  1164. }
  1165.  
  1166. /* Atelier Cave Dark */
  1167. .night .hljs-comment,
  1168. .night .hljs-quote {
  1169. color:#7e7887 !important
  1170. }
  1171. .night .hljs-variable,
  1172. .night .hljs-template-variable,
  1173. .night .hljs-attribute,
  1174. .night .hljs-regexp,
  1175. .night .hljs-link,
  1176. .night .hljs-tag,
  1177. .night .hljs-name,
  1178. .night .hljs-selector-id,
  1179. .night .hljs-selector-class {
  1180. color:#be4678 !important
  1181. }
  1182. .night .hljs-number,
  1183. .night .hljs-meta,
  1184. .night .hljs-built_in,
  1185. .night .hljs-builtin-name,
  1186. .night .hljs-literal,
  1187. .night .hljs-type,
  1188. .night .hljs-params {
  1189. color:#aa573c !important
  1190. }
  1191. .night .hljs-string,
  1192. .night .hljs-symbol,
  1193. .night .hljs-bullet {
  1194. color:#2a9292 !important
  1195. }
  1196. .night .hljs-title,
  1197. .night .hljs-section {
  1198. color:#576ddb !important
  1199. }
  1200. .night .hljs-keyword,
  1201. .night .hljs-selector-tag {
  1202. color:#955ae7 !important
  1203. }
  1204. .night .hljs-deletion,
  1205. .night .hljs-addition {
  1206. color:#19171c !important;
  1207. display:inline-block !important;
  1208. width:100% !important
  1209. }
  1210. .night .hljs-deletion {
  1211. background-color:#be4678 !important
  1212. }
  1213. .night .hljs-addition {
  1214. background-color:#2a9292 !important
  1215. }
  1216. .night .hljs {
  1217. display:block !important;
  1218. overflow-x:auto !important;
  1219. background:#19171c !important;
  1220. color:#8b8792 !important;
  1221. /*padding:0.5em !important*/
  1222. }
  1223. .night .hljs-emphasis {
  1224. font-style:italic !important
  1225. }
  1226. .night .hljs-strong {
  1227. font-weight:bold !important
  1228. }
  1229. `;
  1230. }
  1231.  
  1232. if (FLAGS.USERSTYLE_CONFIG_INTERFACE) {
  1233. userStyle += `
  1234. .config_button {
  1235. box-sizing: border-box;
  1236. position: fixed;
  1237. width: ${BUTTON_SIZE}px;
  1238. height: ${BUTTON_SIZE2}px;
  1239. right: ${BUTTON_SIZE}px;
  1240. bottom: ${FLAGS.NIGHT_MODE ? BUTTON_SIZE4 : BUTTON_SIZE}px;
  1241. z-index: 10000;
  1242. background: -webkit-linear-gradient(top, #aaa 50%, transparent 50%);
  1243. background: -moz-linear-gradient(top, #aaa 50%, transparent 50%);
  1244. background: -moz-linear-gradient(top, #aaa 50%, transparent 50%);
  1245. background-size: 10px 10px;
  1246. transition: background 0.1s ease-out;
  1247. }
  1248.  
  1249. .config_button:hover {
  1250. background: -webkit-linear-gradient(top, #333 50%, transparent 50%);
  1251. background: -moz-linear-gradient(top, #333 50%, transparent 50%);
  1252. background: -moz-linear-gradient(top, #333 50%, transparent 50%);
  1253. background-size: 10px 10px;
  1254. }
  1255.  
  1256. .night .config_button {
  1257. background: -webkit-linear-gradient(top, #515151 50%, transparent 50%);
  1258. background: -moz-linear-gradient(top, #515151 50%, transparent 50%);
  1259. background: -moz-linear-gradient(top, #515151 50%, transparent 50%);
  1260. background-size: 10px 10px;
  1261. }
  1262.  
  1263. .night .config_button:hover {
  1264. background: -webkit-linear-gradient(top, #9e9e9e 50%, transparent 50%);
  1265. background: -moz-linear-gradient(top, #9e9e9e 50%, transparent 50%);
  1266. background: -moz-linear-gradient(top, #9e9e9e 50%, transparent 50%);
  1267. background-size: 10px 10px;
  1268. }
  1269.  
  1270. .config_frame {
  1271. box-sizing: border-box;
  1272. position: fixed;
  1273. right: 80px;
  1274. bottom: 32px;
  1275. z-index: 10000;
  1276. border: 1px solid #aaa;
  1277. padding: 8px;
  1278. background: #f7f7f7;
  1279. -webkit-user-select: none;
  1280. -moz-user-select: none;
  1281. -ms-user-select: none;
  1282. user-select: none;
  1283. overflow-y: auto;
  1284. max-height: calc(100vh - 64px);
  1285. min-width: 390px;
  1286. }
  1287. .config_frame label:hover {
  1288. cursor: pointer;
  1289. background: rgba(128, 128, 128, 0.3);
  1290. }
  1291. .config_frame input {
  1292. cursor: pointer;
  1293. position: absolute;
  1294. opacity: 0;
  1295. }
  1296. .config_frame input + span:before {
  1297. content: '';
  1298. display: inline-block;
  1299. width: 0.5em;
  1300. height: 0.5em;
  1301. margin: 0 0.4em 0.1em 0.3em;
  1302. outline: 1px solid currentcolor;
  1303. outline-offset: 1px;
  1304. }
  1305. .config_frame input:checked + span:before {
  1306. background: currentcolor;
  1307. }
  1308. .night .config_frame {
  1309. background: #22272B;
  1310. border-color: #393d41;
  1311. }
  1312. `;
  1313. }
  1314.  
  1315. userStyleEl.innerHTML = userStyle;
  1316.  
  1317. const navigatorEdge = /Edge/.test(navigator.userAgent);
  1318.  
  1319. function readyHead(fn) {
  1320. if (document.body) { // если есть body, значит head готов
  1321. fn();
  1322. } else if (document.documentElement && !navigatorEdge) {
  1323. const observer = new MutationObserver(() => {
  1324. if (document.body) {
  1325. observer.disconnect();
  1326. fn();
  1327. }
  1328. });
  1329. observer.observe(document.documentElement, { childList: true });
  1330. } else {
  1331. // рекурсивное ожидание появления DOM
  1332. setTimeout(() => readyHead(fn), 16);
  1333. }
  1334. }
  1335.  
  1336. readyHead(() => {
  1337. if (document.getElementById('habrafixmarker')) return;
  1338. if (FLAGS.USERSTYLE) document.head.appendChild(userStyleEl);
  1339. if (FLAGS.NIGHT_MODE && userConfig.getItem('night_mode')) {
  1340. document.documentElement.classList.add('night');
  1341. }
  1342. });
  1343.  
  1344. function ready(fn) {
  1345. const { readyState } = document;
  1346. if (readyState === 'loading') {
  1347. document.addEventListener('DOMContentLoaded', () => {
  1348. fn();
  1349. });
  1350. } else {
  1351. fn();
  1352. }
  1353. }
  1354.  
  1355. ready(() => {
  1356. if (document.getElementById('habrafixmarker')) return;
  1357. if (FLAGS.COMMENTS_MD) {
  1358. const mdSelectorEl = document.getElementById('comment_markdown');
  1359. if (mdSelectorEl) {
  1360. if (userConfig.getItem('comment_markdown')) mdSelectorEl.checked = true;
  1361. mdSelectorEl.addEventListener('input', () => {
  1362. userConfig.setItem('comment_markdown', mdSelectorEl.checked);
  1363. });
  1364. }
  1365. }
  1366.  
  1367. if (FLAGS.HTSO_BTN) {
  1368. const commentForm = document.getElementById('comment-form');
  1369. if (commentForm) {
  1370. const toolbar = commentForm.querySelector('.tm-editor__toolbar');
  1371. if (toolbar) {
  1372. const item = document.createElement('li');
  1373. item.classList.add('wysiwyg-toolbar__item');
  1374. item.innerHTML = `
  1375. <button type="button" class="btn btn_wysiwyg" tabindex="0" title="Загрузка картинок"
  1376. onclick="window.open('//hsto.org', '_blank').focus();">
  1377. <svg class="icon-svg icon-svg_spoiler_wysiwyg" aria-hidden="true" aria-labelledby="title"
  1378. version="1.1" role="img" width="24" viewBox="0 0 100 82.9">
  1379. <path
  1380. d="M77.9,82.9H5.6c-3.4,0-5.6-2.4-5.6-5.2V21.9c0-3.5,2.1-6.1,
  1381. 5.6-6.1H50v11.1H11v45h61V54.8l12,0v22.9 C84,80.5,81.2,82.9,77.9,82.9L77.9,82.9z">
  1382. </path>
  1383. <polygon points="16.5,66.9 39.8,44.6 50.2,54.4 61.5,39.6 67,50.2 67,66.9 "></polygon>
  1384. <path
  1385. d="M28,44.4c-3.2,0-5.7-2.6-5.7-5.7c0-3.2,2.6-5.8,5.7-5.8c3.2,0,5.8,2.6,5.8,
  1386. 5.8C33.8,41.9,31.2,44.4,28,44.4 L28,44.4z">
  1387. </path>
  1388. <polygon points="84,21.9 84,44 72,44 72,21.9 56.1,21.9 78.1,0 100,21.9 "></polygon>
  1389. </svg>
  1390. </button>
  1391. `;
  1392. toolbar.appendChild(item);
  1393. }
  1394. }
  1395. }
  1396.  
  1397. if (FLAGS.SUBS_BTN) {
  1398. const userBtn = document.querySelector('.tabs-menu__item_link');
  1399. const isUserPage = /^\/(ru|en)\/users\/[^/]+\//.test(window.location.pathname);
  1400. if (userBtn && isUserPage) {
  1401. const bar = userBtn.parentElement;
  1402.  
  1403. const tab = document.createElement('a');
  1404. const isSubs = /subscription\/$/.test(window.location.pathname);
  1405. tab.classList.add('tabs-menu__item', 'tabs-menu__item_link');
  1406. tab.href = `${userBtn.href}subscription/`;
  1407. tab.innerHTML = `<h3 class="tabs-menu__item-text ${isSubs ? 'tabs-menu__item-text_active' : ''}">Он читает</h3>`;
  1408. bar.appendChild(tab);
  1409.  
  1410. const tab2 = document.createElement('a');
  1411. const isFols = /followers\/$/.test(window.location.pathname);
  1412. tab2.classList.add('tabs-menu__item', 'tabs-menu__item_link');
  1413. tab2.href = `${userBtn.href}subscription/followers/`;
  1414. tab2.innerHTML = `<h3 class="tabs-menu__item-text ${isFols ? 'tabs-menu__item-text_active' : ''}">Его читают</h3>`;
  1415. bar.appendChild(tab2);
  1416. }
  1417. }
  1418.  
  1419. // надо ли ещё
  1420. Array.from(document.querySelectorAll('iframe[src^="https://codepen.io/"]'))
  1421. .map(el => el.setAttribute('scrolling', 'no'));
  1422.  
  1423. // остановка гифок по клику и воспроизведение при повторном клике
  1424. function toggleGIF(el) {
  1425. // если атрибут со старым линком пуст или отсутствует
  1426. if (!el.dataset.oldSrc) {
  1427. // заменим ссылку на data-url-svg с треугольником в круге
  1428. const w = Math.max(el.clientWidth || 256, 16);
  1429. const h = Math.max(el.clientHeight || 128, 16);
  1430. const cx = w / 2;
  1431. const cy = h / 2;
  1432. const r = Math.min(w, h) / 4;
  1433. const ax = (r * 61) / 128;
  1434. const by = (r * 56) / 128;
  1435. const bx = (r * 35) / 128;
  1436. const svg = `data:image/svg+xml;utf8,
  1437. <svg width='${w}' height='${h}' baseProfile='full' xmlns='http://www.w3.org/2000/svg'>
  1438. <rect x='0' y='0' width='${w}' height='${h}' fill='${FLAGS.GIF_STOP_COLOR_BG}'/>
  1439. <circle cx='${cx}' cy='${cy}' r='${r}' fill='${FLAGS.GIF_STOP_COLOR_FG}'/>
  1440. <polygon points='${cx + ax} ${cy} ${cx - bx} ${cy - by} ${cx - bx} ${cy + by}' fill='${FLAGS.GIF_STOP_COLOR_BG}' />
  1441. </svg>
  1442. `;
  1443. el.dataset.oldSrc = el.getAttribute('src'); // eslint-disable-line no-param-reassign
  1444. el.setAttribute('src', svg);
  1445. } else if (FLAGS.GIF_STOP_OVERTYPE) {
  1446. // иначе поставим svg с троеточием
  1447. const w = el.clientWidth;
  1448. const h = el.clientHeight;
  1449. const cx = w / 2;
  1450. const cy = h / 2;
  1451. const r = Math.min(w, h) / 4;
  1452. const r2 = r / 4;
  1453. const svg = `data:image/svg+xml;utf8,
  1454. <svg width='${w}' height='${h}' baseProfile='full' xmlns='http://www.w3.org/2000/svg'>
  1455. <rect x='0' y='0' width='${w}' height='${h}' fill='${FLAGS.GIF_STOP_COLOR_BG}'/>
  1456. <circle cx='${cx - r}' cy='${cy}' r='${r2}' fill='${FLAGS.GIF_STOP_COLOR_FG}'/>
  1457. <circle cx='${cx}' cy='${cy}' r='${r2}' fill='${FLAGS.GIF_STOP_COLOR_FG}'/>
  1458. <circle cx='${cx + r}' cy='${cy}' r='${r2}' fill='${FLAGS.GIF_STOP_COLOR_FG}'/>
  1459. </svg>
  1460. `;
  1461. el.setAttribute('src', svg);
  1462. // когда отрендерится троеточие, можно менять на исходную гифку
  1463. setTimeout(() => {
  1464. if (el.dataset.oldSrc) {
  1465. el.setAttribute('src', el.dataset.oldSrc);
  1466. el.dataset.oldSrc = ''; // eslint-disable-line no-param-reassign
  1467. }
  1468. }, 100);
  1469. } else {
  1470. const img = document.createElement('img');
  1471. img.setAttribute('src', el.dataset.oldSrc);
  1472. if (el.hasAttribute('align')) {
  1473. img.setAttribute('align', el.getAttribute('align'));
  1474. }
  1475. el.parentNode.insertBefore(img, el);
  1476. img.onclick = () => toggleGIF(img); // eslint-disable-line no-param-reassign
  1477. el.parentNode.removeChild(el);
  1478. }
  1479. }
  1480.  
  1481. if (FLAGS.GIF_STOP) {
  1482. Array.from(document.querySelectorAll('.post__text img[src$=".gif"], .comment__message img[src$=".gif"]'))
  1483. .filter((el) => {
  1484. const excludes = [
  1485. 'https://habrastorage.org/storage3/976/d3e/38a/976d3e38a34b003f86f91795524af9f8.gif',
  1486. 'https://habrastorage.org/storage3/2e2/522/737/2e2522737ec404a9f76047e108dfaea0.gif',
  1487. 'https://habrastorage.org/getpro/habr/post_images/d4b/289/ef0/d4b289ef0a00e969108c25d0c3d75f58.gif',
  1488. ];
  1489. return !excludes.includes(el.getAttribute('src'));
  1490. })
  1491. .forEach((el) => {
  1492. if (FLAGS.GIF_STOP_ONLOAD) toggleGIF(el);
  1493. el.classList.add('habrafix_gif-stop');
  1494. el.onclick = () => toggleGIF(el); // eslint-disable-line no-param-reassign
  1495. });
  1496. }
  1497.  
  1498. // счетчики кармы
  1499. if (FLAGS.KARMA_DETAILS) {
  1500. Array.from(document.querySelectorAll('.user-info__stats-item.stacked-counter')).forEach((itemCounter) => {
  1501. itemCounter.style.marginRight = '16px'; // eslint-disable-line no-param-reassign
  1502. });
  1503. Array.from(document.querySelectorAll('.page-header__stats_karma')).forEach((karmaEl) => {
  1504. karmaEl.style.width = 'auto'; // eslint-disable-line no-param-reassign
  1505. karmaEl.style.minWidth = `${KARMA_WIDTH}px`; // eslint-disable-line no-param-reassign
  1506. });
  1507. Array.from(document.querySelectorAll(`
  1508. .stacked-counter[href="https://habr.com/ru/info/help/karma/"],
  1509. .stacked-counter[href="https://habr.com/en/info/help/karma/"]
  1510. `)).forEach((counterEl) => {
  1511. let total = parseInt(counterEl.title, 10);
  1512. const scoreEl = counterEl.querySelector('.stacked-counter__value');
  1513. if (!scoreEl || !total) return;
  1514. counterEl.style.width = 'auto'; // eslint-disable-line no-param-reassign
  1515. counterEl.style.minWidth = `${KARMA_WIDTH}px`; // eslint-disable-line no-param-reassign
  1516. const score = parseFloat(scoreEl.innerHTML.replace('–', '-').replace(',', '.'), 10);
  1517. if (score > total) total = score;
  1518. const likes = (total + score) / 2;
  1519. const percent = Math.round((100 * likes) / total);
  1520. const details = `&nbsp;= ${total} × (${percent} ${100 - percent})%`;
  1521. const detailsEl = document.createElement('span');
  1522. detailsEl.innerHTML = details;
  1523. detailsEl.style.color = '#545454';
  1524. detailsEl.style.fontFamily = '"-apple-system",BlinkMacSystemFont,Arial,sans-serif';
  1525. detailsEl.style.fontSize = '13px';
  1526. detailsEl.style.fontWeight = 'normal';
  1527. detailsEl.style.verticalAlign = 'middle';
  1528. scoreEl.appendChild(detailsEl);
  1529. counterEl.title += `, ${(likes).toFixed(2)} плюсов и ${(total - likes).toFixed(2)} минусов`; // eslint-disable-line no-param-reassign
  1530. });
  1531. }
  1532.  
  1533. // счетчики рейтинга с подробностями
  1534. const scoresMap = new Map();
  1535.  
  1536. class Score {
  1537. constructor(el) {
  1538. this.el = el;
  1539. this.parentEl = el.parentNode;
  1540. const data = this.constructor.parse(el);
  1541. this.rating = data.rating;
  1542. this.total = data.total;
  1543. this.likes = data.likes;
  1544. this.dislikes = data.dislikes;
  1545. this.isDetailed = false;
  1546. this.observer = new MutationObserver(() => this.update());
  1547. }
  1548.  
  1549. setDetails(isDetailed) {
  1550. if (this.isDetailed === isDetailed) return;
  1551. this.isDetailed = isDetailed;
  1552. this.update();
  1553. }
  1554.  
  1555. update() {
  1556. const newChild = this.parentEl.querySelector('.voting-wjt__counter, .post-stats__result-counter');
  1557. if (!newChild) return;
  1558. this.el = newChild;
  1559. const data = this.constructor.parse(this.el);
  1560. this.rating = data.rating;
  1561. this.total = data.total;
  1562. this.likes = data.likes;
  1563. this.dislikes = data.dislikes;
  1564. this.observer.disconnect();
  1565. if (this.isDetailed) {
  1566. this.details();
  1567. } else {
  1568. this.simply();
  1569. }
  1570. this.observer.observe(this.parentEl, { childList: true });
  1571. }
  1572.  
  1573. static parse(el) {
  1574. let [, likes, dislikes] = el
  1575. .attributes.title.textContent
  1576. .match(/[0-9]+/g).map(Number);
  1577. let total = likes + dislikes;
  1578. let [, sign, rating] = el.innerHTML.match(/([–]?)(\d+)/); // eslint-disable-line prefer-const
  1579. rating = Number(rating);
  1580. if (sign) rating = -rating;
  1581. // не знаю что там происходит при голосовании, так что на всякий случай
  1582. const diff = rating - (likes - dislikes);
  1583. if (diff < 0) {
  1584. total += Math.abs(diff);
  1585. dislikes += Math.abs(diff);
  1586. } else if (diff > 0) {
  1587. total += diff;
  1588. likes += diff;
  1589. }
  1590. return {
  1591. rating,
  1592. total,
  1593. likes,
  1594. dislikes,
  1595. };
  1596. }
  1597.  
  1598. simply() {
  1599. let innerHTML = '';
  1600. if (this.rating > 0) {
  1601. innerHTML = `+${this.rating}`;
  1602. } else if (this.rating < 0) {
  1603. innerHTML = `–${Math.abs(this.rating)}`;
  1604. } else {
  1605. innerHTML = '0';
  1606. }
  1607. this.el.innerHTML = innerHTML;
  1608. }
  1609.  
  1610. details() {
  1611. let innerHTML = '';
  1612. if (this.rating > 0) {
  1613. innerHTML = `+${this.rating}`;
  1614. } else if (this.rating < 0) {
  1615. innerHTML = `–${Math.abs(this.rating)}`;
  1616. } else {
  1617. innerHTML = '0';
  1618. }
  1619. if (this.total !== 0) {
  1620. let details = '';
  1621. if (FLAGS.RATING_DETAILS_PN) {
  1622. const percent = Math.round((100 * this.likes) / this.total);
  1623. details = `&nbsp;= ${this.total} × (${percent} ${100 - percent})%`;
  1624. } else {
  1625. details = `&nbsp;= ${this.likes} ${this.dislikes}`;
  1626. }
  1627. innerHTML += ` <span style='color: #545454; font-weight: normal'>${details}</span>`;
  1628. }
  1629. this.el.innerHTML = innerHTML;
  1630. }
  1631. }
  1632.  
  1633. // парсим их
  1634. Array.from(document.querySelectorAll('.voting-wjt__counter, .post-stats__result-counter')).forEach((el) => {
  1635. scoresMap.set(el, new Score(el));
  1636. });
  1637.  
  1638. // добавляем подробностей
  1639. if (FLAGS.RATING_DETAILS) {
  1640. if (FLAGS.RATING_DETAILS_ONCLICK) {
  1641. const isDetailed = userConfig.getItem('scores_details');
  1642. if (isDetailed) scoresMap.forEach(score => score.setDetails(isDetailed));
  1643. scoresMap.forEach((score) => {
  1644. score.el.onclick = () => { // eslint-disable-line no-param-reassign
  1645. const nowDetailed = userConfig.shiftItem('scores_details');
  1646. scoresMap.forEach(s => s.setDetails(nowDetailed));
  1647. };
  1648. });
  1649. } else {
  1650. scoresMap.forEach(score => score.setDetails(true));
  1651. }
  1652. }
  1653.  
  1654. // метки времени и работа с ними
  1655. const pageLoadTime = new Date();
  1656. const monthNames = [
  1657. 'января', 'февраля', 'марта',
  1658. 'апреля', 'мая', 'июня',
  1659. 'июля', 'августа', 'сентября',
  1660. 'октября', 'ноября', 'декабря',
  1661. ];
  1662. const monthNamesEng = [
  1663. 'January', 'February', 'March',
  1664. 'April', 'May', 'June',
  1665. 'July', 'August', 'September',
  1666. 'October', 'November', 'December',
  1667. ];
  1668.  
  1669. class HabraTime {
  1670. constructor(el, parent) {
  1671. this.el = el;
  1672. this.parent = parent;
  1673. this.attrDatetime = this.constructor.getAttributeDatetime(el);
  1674. this.date = new Date(this.attrDatetime);
  1675. }
  1676.  
  1677. // вот было бы хорошо, если б на хабре были datetime атрибуты
  1678. static getAttributeDatetime(el) {
  1679. const imagination = el.getAttribute('datetime') || el.getAttribute('data-time_published');
  1680. if (imagination) return imagination;
  1681.  
  1682. let recently;
  1683. let day;
  1684. let month;
  1685. let year;
  1686. let time;
  1687. let isEng = false;
  1688. let meridiem;
  1689. if (/^\d\d:\d\d$/.test(el.innerHTML)) {
  1690. [, time] = el.innerHTML.match(/(\d\d:\d\d)/);
  1691. recently = 'сегодня';
  1692. } else if (/^\d\d:\d\d (AM|PM)$/.test(el.innerHTML)) {
  1693. [, time, meridiem] = el.innerHTML.match(/(\d\d:\d\d) (AM|PM)/);
  1694. recently = 'сегодня';
  1695. } else if (/at/.test(el.innerHTML)) {
  1696. isEng = true;
  1697. const re = /((today|yesterday)|([A-z]+) (\d+), (\d+)) at (\d\d:\d\d) (AM|PM)/;
  1698. [,,
  1699. recently,
  1700. month, day, year,
  1701. time,
  1702. meridiem,
  1703. ] = el.innerHTML.match(re);
  1704.  
  1705. if (recently === 'today') {
  1706. recently = 'сегодня';
  1707. } else if (recently === 'yesterday') {
  1708. recently = 'вчера';
  1709. }
  1710. } else {
  1711. const re = /((сегодня|вчера)|(\d+)[ .]([а-я]+|\d+)[ .]?(\d+)?) в (\d\d:\d\d)/;
  1712. [,,
  1713. recently,
  1714. day, month, year,
  1715. time,
  1716. ] = el.innerHTML.match(re);
  1717. }
  1718.  
  1719. const [, h, m] = time.match(/(\d\d):(\d\d)/);
  1720. if (meridiem === 'PM') {
  1721. time = `${Number(h === '12' ? 0 : h) + 12}:${m}`;
  1722. } else if (meridiem === 'AM') {
  1723. time = `${h === '12' ? '00' : h}:${m}`;
  1724. }
  1725.  
  1726. // и местное время
  1727. let moscow;
  1728. if (recently || year === undefined) {
  1729. const offsetMoscow = 3 * 60 * 60 * 1000;
  1730. const yesterdayShift = (recently === 'вчера') ? 24 * 60 * 60 * 1000 : 0;
  1731. const offset = pageLoadTime.getTimezoneOffset() * 60 * 1000;
  1732. const value = (pageLoadTime - yesterdayShift) + offsetMoscow + offset;
  1733. moscow = new Date(value);
  1734. }
  1735.  
  1736. if (recently) {
  1737. day = moscow.getDate();
  1738. month = moscow.getMonth() + 1;
  1739. } else if (month.length !== 2) {
  1740. month = (isEng ? monthNamesEng : monthNames).indexOf(month) + 1;
  1741. } else {
  1742. month = +month;
  1743. }
  1744.  
  1745. if (day < 10) day = `0${+day}`;
  1746. if (month < 10) month = `0${month}`;
  1747. if (year < 100) year = `20${year}`;
  1748. if (year === undefined) year = moscow.getFullYear();
  1749.  
  1750. return `${year}-${month}-${day}T${time}+03:00`;
  1751. }
  1752.  
  1753. absolute() {
  1754. let result = '';
  1755.  
  1756. const time = this.date;
  1757. const day = time.getDate();
  1758. const month = time.getMonth();
  1759. const monthName = monthNames[month];
  1760. const year = time.getFullYear();
  1761. const hours = time.getHours();
  1762. const minutes = time.getMinutes();
  1763.  
  1764. const now = new Date();
  1765. const nowDay = now.getDate();
  1766. const nowMonth = now.getMonth();
  1767. const nowYear = now.getFullYear();
  1768.  
  1769. const yesterday = new Date(now - (24 * 60 * 60 * 1000));
  1770. const yesterdayDay = yesterday.getDate();
  1771. const yesterdayMonth = yesterday.getMonth();
  1772. const yesterdayYear = yesterday.getFullYear();
  1773.  
  1774. const hhmm = `${hours}:${minutes >= 10 ? minutes : `0${minutes}`}`;
  1775.  
  1776. const isToday =
  1777. day === nowDay &&
  1778. month === nowMonth &&
  1779. year === nowYear;
  1780. const isYesterday =
  1781. day === yesterdayDay &&
  1782. month === yesterdayMonth &&
  1783. year === yesterdayYear;
  1784.  
  1785. if (isToday) {
  1786. result = `сегодня в ${hhmm}`;
  1787. } else if (isYesterday) {
  1788. result = `вчера в ${hhmm}`;
  1789. } else if (nowYear === year) {
  1790. result = `${day} ${monthName} в ${hhmm}`;
  1791. } else {
  1792. result = `${day} ${monthName} ${year} в ${hhmm}`;
  1793. }
  1794.  
  1795. return result;
  1796. }
  1797.  
  1798. static relative(milliseconds) {
  1799. let result = '';
  1800.  
  1801. const pluralForm = (n, forms) => {
  1802. if (n % 10 === 1 && n % 100 !== 11) return forms[0];
  1803. if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return forms[1];
  1804. return forms[2];
  1805. };
  1806.  
  1807. const formats = [
  1808. ['год', 'года', 'лет'],
  1809. ['месяц', 'месяца', 'месяцев'],
  1810. ['день', 'дня', 'дней'],
  1811. ['час', 'часа', 'часов'],
  1812. ['минуту', 'минуты', 'минут'],
  1813. ];
  1814.  
  1815. const minutes = milliseconds / 60000;
  1816. const hours = minutes / 60;
  1817. const days = hours / 24;
  1818. const months = days / 30;
  1819. const years = months / 12;
  1820. const idx = [years, months, days, hours, minutes].findIndex(x => x >= 1);
  1821.  
  1822. if (idx === -1) {
  1823. result = 'несколько секунд';
  1824. } else {
  1825. const value = Math.floor([years, months, days, hours, minutes][idx]);
  1826. const forms = formats[idx];
  1827. const form = pluralForm(value, forms);
  1828. result = `${value} ${form}`;
  1829. }
  1830. return result;
  1831. }
  1832.  
  1833. fromNow() {
  1834. const diff = Math.abs(Date.now() - this.date);
  1835. return `${this.constructor.relative(diff)} назад`;
  1836. }
  1837.  
  1838. fromParent() {
  1839. const diff = Math.abs(this.date - this.parent.date);
  1840. return `через ${this.constructor.relative(diff)}`;
  1841. }
  1842.  
  1843. static datetimeToMsk(datetime) {
  1844. const [, yyyy, mm, dd, h, m] = datetime
  1845. .match(/([0-9]+)-([0-9]+)-([0-9]+)T([0-9]+):([0-9]+)/);
  1846. return `${Number(dd)} ${monthNames[mm - 1]} ${yyyy} в ${h}:${m}`;
  1847. }
  1848. }
  1849.  
  1850. // собираем метки времени
  1851. const datesMap = new Map();
  1852. const megapostTimeEl = document.querySelector('.megapost-head__meta > .list_inline > .list__item');
  1853. (megapostTimeEl ? [megapostTimeEl] : [])
  1854. .concat(Array.from(document.querySelectorAll(`
  1855. .post__time,
  1856. .preview-data__time-published,
  1857. time.comment__date-time_published,
  1858. .tm-post__date,
  1859. .user-message__date-time,
  1860. .news-topic__attr_date-time
  1861. `))).forEach((el) => {
  1862. datesMap.set(el, new HabraTime(el));
  1863. });
  1864.  
  1865. function updateTime(el) {
  1866. datesMap.forEach((habraTime) => {
  1867. if (
  1868. !habraTime.el ||
  1869. !document.body.contains(habraTime.el) ||
  1870. (el && el !== habraTime.el)
  1871. ) return;
  1872. let type;
  1873. let otherTypes;
  1874. if (habraTime.parent) {
  1875. type = userConfig.config.time_comments;
  1876. otherTypes = userConfig.model.time_comments
  1877. .filter(str => str !== type);
  1878. } else {
  1879. type = userConfig.config.time_publications;
  1880. otherTypes = userConfig.model.time_publications
  1881. .filter(str => str !== type);
  1882. }
  1883. const title = otherTypes.map(otherType => habraTime[otherType]()).join(', ');
  1884. habraTime.el.innerHTML = habraTime[type](); // eslint-disable-line no-param-reassign
  1885. habraTime.el.setAttribute('title', title);
  1886. });
  1887. }
  1888.  
  1889. if (FLAGS.TIME_DETAILS) {
  1890. datesMap.forEach((habraTime) => {
  1891. habraTime.el.setAttribute(
  1892. 'style',
  1893. 'cursor: pointer; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; user-select: none;',
  1894. );
  1895. habraTime.el.onclick = () => { // eslint-disable-line no-param-reassign
  1896. if (habraTime.parent) {
  1897. userConfig.shiftItem('time_comments');
  1898. } else {
  1899. userConfig.shiftItem('time_publications');
  1900. }
  1901. updateTime();
  1902. };
  1903. });
  1904. // подождём, когда дерево комментариев будет построено
  1905. // у некоторых меток времени будут установлены родители
  1906. // тогда и обновим их тексты
  1907. setTimeout(updateTime, 100);
  1908. setInterval(updateTime, 30 * 1000);
  1909. }
  1910.  
  1911. // время публикации, понадобится для корня древа комментариев
  1912. let datePublication = datesMap.get(megapostTimeEl || document.querySelector('.post__time'));
  1913. // если нету публикации поищем самую раннюю метку времени
  1914. if (!datePublication) {
  1915. datePublication = { date: pageLoadTime };
  1916. datesMap.forEach((date) => {
  1917. if (date.date < datePublication.date) datePublication = date;
  1918. });
  1919. }
  1920.  
  1921. if (FLAGS.FIND_COMMENTS) {
  1922. const commentsList = document.querySelector('.user_comments');
  1923. const match = document.location.pathname.match(/users\/([^/]+)\/comments/i);
  1924. if (match && commentsList) {
  1925. const nickname = match[1];
  1926. const originalTitle = document.title;
  1927.  
  1928. const searchForm = document.createElement('div');
  1929. searchForm.classList.add('search-form', 'search-form_expanded');
  1930. searchForm.style.width = 'auto';
  1931. searchForm.innerHTML = `
  1932. <span class="search-field__icon icon-svg_search" style="left: 0;"><svg class="icon-svg" width="32" height="32"
  1933. viewBox="0 0 32 32" aria-hidden="true" version="1.1" role="img"><path d="M21.416 13.21c0 4.6-3.65 8.34-8.14
  1934. 8.34S5.11 17.81 5.11 13.21c0-4.632 3.65-8.373 8.167-8.373 4.488 0 8.14 3.772 8.14 8.372zm1.945
  1935. 7.083c1.407-2.055 2.155-4.57 2.155-7.084C25.515 6.277 20.04.665 13.277.665S1.04 6.278 1.04 13.21c0 6.93 5.475
  1936. 12.542 12.237 12.542 2.454 0 4.907-.797 6.942-2.208l7.6 7.79 3.14-3.22-7.6-7.82z"></path></svg></span>
  1937. <span class="search-field__icon icon-svg_loading" style="left: 0;"><svg class="icon-svg" width="40" height="40"
  1938. viewBox="0 0 100 100" enable-background="new 0 0 0 0"><circle cx="50" cy="50" fill="none" stroke="#333333"
  1939. stroke-width="4" r="20" stroke-dasharray="94.24777960769379 33.41592653589793"
  1940. transform="rotate(88.5132 50 50)"><animateTransform attributeName="transform" type="rotate" calcMode="linear"
  1941. values="0 50 50;360 50 50" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animateTransform>
  1942. </circle></svg></span>
  1943. <label
  1944. id="comments_search_label"
  1945. class="search-form__field-wrapper"
  1946. style="background: linear-gradient(to right, rgba(84, 142, 170, 0.2) 0%, transparent 0%)"
  1947. >
  1948. <input type="text" class="search-form__field" id="search-comments-field" placeholder="Поиск по тексту комментариев"
  1949. style="position: absolute;background-color: transparent;">
  1950. <button type="button" class="btn btn_search-close" id="search-comments-clear" title="Очистить">
  1951. <svg class="icon-svg icon-svg_navbar-close-search" width="31" height="32" viewBox="0 0 31 32" aria-hidden="true" version="1.1" role="img">
  1952. <path d="M26.67 0L15.217 11.448 3.77 0 0 3.77l11.447 11.45L0 26.666l3.77
  1953. 3.77L15.218 18.99l11.45 11.448 3.772-3.77-11.448-11.45L30.44 3.772z">
  1954. </path>
  1955. </svg>
  1956. </button>
  1957. </label>
  1958. `;
  1959.  
  1960. const notFoundLabel = document.createElement('p');
  1961. notFoundLabel.style.textAlign = 'center';
  1962. notFoundLabel.style.fontSize = '18px';
  1963. notFoundLabel.style.color = 'gray';
  1964. notFoundLabel.style.display = 'none';
  1965. notFoundLabel.textContent = 'Ничего не найдено';
  1966.  
  1967. const commentsSubList = document.createElement('ul');
  1968. commentsSubList.classList.add('content-list', 'content-list_comments');
  1969. commentsSubList.id = 'search-comments';
  1970. commentsSubList.style.display = 'none';
  1971. commentsList.insertBefore(commentsSubList, commentsList.firstChild);
  1972. commentsList.insertBefore(notFoundLabel, commentsList.firstChild);
  1973. commentsList.insertBefore(searchForm, commentsList.firstChild);
  1974.  
  1975. const Progress = {
  1976. set(value) {
  1977. if (value === 1) {
  1978. document.title = 'Поиск завершён';
  1979. this.setProgressBar(0);
  1980. } else {
  1981. const percent = Math.round(value * 100);
  1982. document.title = `${percent}%, идёт поиск`;
  1983. this.setProgressBar(value);
  1984. }
  1985. },
  1986. setProgressBar(value) {
  1987. const percent = value * 100;
  1988. document.getElementById('comments_search_label').style.background = `
  1989. linear-gradient(to right, rgba(84, 142, 170, 0.2) ${percent}%, transparent ${percent}%)
  1990. `;
  1991. },
  1992. reset() {
  1993. this.setProgressBar(0);
  1994. document.title = originalTitle;
  1995. },
  1996. };
  1997.  
  1998. const makeComment = (comment) => {
  1999. const commentEl = document.createElement('li');
  2000. commentEl.classList.add('content-list__item', 'content-list__item_comment', 'content-list__item_comment-plain');
  2001. let ratingClass = '';
  2002. let rating = '0';
  2003. if (comment.score > 0) {
  2004. rating = `+${comment.score}`;
  2005. ratingClass = 'voting-wjt__counter_positive';
  2006. } else if (comment.score < 0) {
  2007. rating = `–${Math.abs(comment.score)}`;
  2008. ratingClass = 'voting-wjt__counter_negative';
  2009. }
  2010. let avatar;
  2011. if (comment.avatar === 'https://habr.com/images/avatars/stub-user-middle.gif') {
  2012. avatar = `<svg class="default-image default-image_mini default-image_green" width="24"
  2013. height="24"><use xlink:href="https://habr.com/images/1558430991/common-svg-sprite.svg#slug"></use></svg>`;
  2014. } else {
  2015. avatar = `<img src="${comment.avatar}" class="user-info__image-pic user-info__image-pic_small" width="24" height="24">`;
  2016. }
  2017. commentEl.innerHTML = `
  2018. <div class="comment__post-title">
  2019. <a href="${comment.post.url}" class="comment__post-link">${comment.post.title}</a>
  2020. <div class="comment__post-footer">
  2021. <a href="${comment.post.url}#comments">
  2022. <svg class="icon-svg_comments icon-svg_comments-plain" width="14" height="13">
  2023. <use xlink:href="https://habr.com/images/1556525186/common-svg-sprite.svg#comment"></use>
  2024. </svg>
  2025. <span class="comment__post-comments-counter">${comment.post.comments_count}</span>
  2026. </a>
  2027. </div>
  2028. </div>
  2029. <div class="comment comment_plain" rel="${comment.id}" id="habrafix_comment_${comment.id}">
  2030. <div class="comment__head">
  2031. <a href="https://habr.com/ru/users/${comment.author.login}/"
  2032. class="user-info user-info_inline" rel="user-popover" data-user-login="${comment.author.login}">
  2033. ${avatar}
  2034. <span class="user-info__nickname user-info__nickname_small user-info__nickname_comment">${comment.author.login}</span>
  2035. </a>
  2036. <svg class="icon_comment-edit" title="Комментарий был изменен"
  2037. style="display: ${comment.time_changed !== '0' ? 'block' : 'none'}" width="12"
  2038. height="12"><use xlink:href="https://habr.com/images/1558430991/common-svg-sprite.svg#pencil"></use></svg>
  2039. <time class="comment__date-time comment__date-time_published"
  2040. datetime="${comment.time_published}">${comment.time_published}</time>
  2041. <ul class="inline-list inline-list_comment-nav">
  2042. <li class="inline-list__item inline-list__item_comment-nav">
  2043. <a href="${comment.post.url}#comment_${comment.id}" class="icon_comment-anchor"
  2044. title="Ссылка на комментарий"><svg width="12" height="12">
  2045. <use xlink:href="https://habr.com/images/1556525186/common-svg-sprite.svg#anchor"></use></svg></a>
  2046. </li>
  2047. <li class="inline-list__item inline-list__item_comment-nav">
  2048. <a href="#" class="icon_comment-bookmark " onclick="comments_add_to_favorite(this)" data-type="3"
  2049. data-id="${comment.id}" data-action="add" title="Добавить в закладки">
  2050. <svg width="12" height="12"><use xlink:href="https://habr.com/images/1556525186/common-svg-sprite.svg#book"></use></svg>
  2051. </a>
  2052. </li>
  2053. </ul>
  2054. <div class="voting-wjt voting-wjt_comments js-comment-vote">
  2055. <span class="voting-wjt__counter ${ratingClass}
  2056. js-score" title="Общий рейтинг ${rating}">${rating}</span>
  2057. </div>
  2058. </div>
  2059. <div class="comment__message">${comment.message}</div>
  2060. </div>`;
  2061. return commentEl;
  2062. };
  2063.  
  2064. const addComment = (comment) => {
  2065. document.getElementById('search-comments').appendChild(makeComment(comment));
  2066. const timeEl = document.querySelector(`#habrafix_comment_${comment.id} time`);
  2067. if (FLAGS.TIME_DETAILS) {
  2068. timeEl.setAttribute(
  2069. 'style',
  2070. 'cursor: pointer; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; user-select: none;',
  2071. );
  2072. timeEl.onclick = () => { // eslint-disable-line no-param-reassign
  2073. userConfig.shiftItem('time_publications');
  2074. updateTime();
  2075. };
  2076. datesMap.set(timeEl, new HabraTime(timeEl));
  2077. updateTime(timeEl);
  2078. } else {
  2079. timeEl.textContent = HabraTime.datetimeToMsk(comment.time_published);
  2080. }
  2081. };
  2082.  
  2083. // let fetchCors = url => fetch(`https://cors.io/?${url}`);
  2084. let fetchCors = url => fetch(url);
  2085.  
  2086. if (typeof GM !== 'undefined' && GM.xmlHttpRequest) {
  2087. fetchCors = url => new Promise((resolve, reject) => GM.xmlHttpRequest({
  2088. method: 'GET',
  2089. url,
  2090. onload(response) {
  2091. resolve({
  2092. json: () => Promise.resolve(JSON.parse(response.responseText)),
  2093. });
  2094. },
  2095. onerror() { reject(); },
  2096. ontimeout() { reject(); },
  2097. }));
  2098. }
  2099.  
  2100. const loadComments = async (page) => {
  2101. const url = `https://m.habr.com/kek/v1/users/${nickname}/comments?comments=true&user=${nickname}&page=${page}`;
  2102. const res = await fetchCors(url);
  2103. const json = await res.json();
  2104. return json.data;
  2105. };
  2106.  
  2107. const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
  2108.  
  2109. const pageCache = {};
  2110. const memoizedLoadComments = async (page) => {
  2111. if (pageCache[page]) {
  2112. await sleep(0);
  2113. return pageCache[page];
  2114. }
  2115. const result = await loadComments(page);
  2116. pageCache[page] = result;
  2117. return result;
  2118. };
  2119.  
  2120. let latestSearchId = 0;
  2121.  
  2122. const removeResultsList = () => {
  2123. document.getElementById('search-comments-field').value = '';
  2124. latestSearchId += 1;
  2125. document.getElementById('search-comments').innerHTML = '';
  2126. document.getElementById('search-comments').style.display = 'none';
  2127. document.getElementById('comments').style.display = '';
  2128. const footer = document.querySelector('.page__footer');
  2129. if (footer) footer.style.display = '';
  2130. Progress.reset();
  2131. notFoundLabel.style.display = 'none';
  2132. searchForm.classList.remove('loading');
  2133. };
  2134.  
  2135. const addResultsList = () => {
  2136. document.getElementById('search-comments').innerHTML = '';
  2137. document.getElementById('search-comments').style.display = '';
  2138. document.getElementById('comments').style.display = 'none';
  2139. const footer = document.querySelector('.page__footer');
  2140. if (footer) footer.style.display = 'none';
  2141. Progress.set(0);
  2142. notFoundLabel.style.display = 'none';
  2143. searchForm.classList.add('loading');
  2144. };
  2145.  
  2146. document.getElementById('search-comments-clear').onclick = removeResultsList;
  2147.  
  2148. // eslint-disable-next-line consistent-return
  2149. const search = async () => {
  2150. latestSearchId += 1;
  2151. const currentSearchId = latestSearchId;
  2152.  
  2153. const text = document.getElementById('search-comments-field').value;
  2154. if (!text) return removeResultsList();
  2155.  
  2156. addResultsList();
  2157.  
  2158. const filter = (comment) => {
  2159. const t = text.toLowerCase();
  2160. const message = comment.message.toLowerCase();
  2161. const title = comment.post.title.toLowerCase();
  2162. return message.includes(t) || title.includes(t);
  2163. };
  2164.  
  2165. let pages = 1;
  2166. for (let curPage = 1; curPage <= pages; curPage += 1) {
  2167. let data;
  2168.  
  2169. while (!data) {
  2170. try {
  2171. // eslint-disable-next-line no-await-in-loop
  2172. data = await memoizedLoadComments(curPage);
  2173. } catch (e) {
  2174. document.title = 'Ошибка сети, повтор...';
  2175. // eslint-disable-next-line no-await-in-loop
  2176. await sleep(5000);
  2177. }
  2178. }
  2179.  
  2180. // eslint-disable-next-line consistent-return
  2181. if (currentSearchId !== latestSearchId) return; // пользователь изменил текст
  2182.  
  2183. pages = pages === 1 ? data.pages : pages;
  2184. Progress.set(curPage / pages);
  2185.  
  2186. data.comments
  2187. .filter(filter)
  2188. .forEach(addComment);
  2189. }
  2190.  
  2191. if (commentsSubList.childNodes.length === 0) notFoundLabel.style.display = '';
  2192. searchForm.classList.remove('loading');
  2193. };
  2194.  
  2195. const awaitEndOfInput = (func, ms) => {
  2196. let timerId;
  2197. return () => {
  2198. clearTimeout(timerId);
  2199. timerId = setTimeout(func, ms);
  2200. };
  2201. };
  2202.  
  2203. document.getElementById('search-comments-field').oninput = awaitEndOfInput(search, 1000);
  2204. }
  2205. }
  2206.  
  2207. // создаем дерево комментариев
  2208. class ItemComment {
  2209. constructor(el, parent) {
  2210. this.parent = parent;
  2211. this.el = el;
  2212. this.lvl = parent.lvl + 1;
  2213. this.id = Number(el.getAttribute('rel'));
  2214. this.commentEl = el.querySelector('.comment');
  2215. if (this.commentEl) {
  2216. this.timeEl = this.commentEl.querySelector('time');
  2217. this.ratingEl = this.commentEl.querySelector('.js-score');
  2218. }
  2219. this.date = datesMap.get(this.timeEl);
  2220. if (this.date) {
  2221. this.date.parent = parent.date;
  2222. } else {
  2223. this.date = parent.date;
  2224. }
  2225. this.votes = scoresMap.get(this.ratingEl) || {
  2226. total: 0, likes: 0, dislikes: 0, rating: 0,
  2227. };
  2228. this.elList = el.querySelector('.content-list_nested-comments');
  2229. }
  2230.  
  2231. existId(id) {
  2232. return !!this.elList.querySelector(id);
  2233. }
  2234.  
  2235. existNew() {
  2236. return !!this.elList.querySelector('.js-comment_new');
  2237. }
  2238.  
  2239. getLength() {
  2240. let { length } = this.list;
  2241. this.list.forEach((node) => {
  2242. length += node.getLength();
  2243. });
  2244. return length;
  2245. }
  2246. }
  2247.  
  2248. class CommentsTree {
  2249. constructor() {
  2250. this.root = {
  2251. isRoot: true,
  2252. date: datePublication,
  2253. lvl: 0,
  2254. elList: document.getElementById('comments-list'),
  2255. list: [],
  2256. };
  2257. }
  2258.  
  2259. static exist() {
  2260. return !!document.getElementById('comments-list');
  2261. }
  2262.  
  2263. update() {
  2264. if (!this.root.elList) return;
  2265. const recAdd = (node) => {
  2266. node.list = Array.from(node.elList.children) // eslint-disable-line no-param-reassign
  2267. .map(el => new ItemComment(el, node));
  2268. node.list.forEach(recAdd);
  2269. };
  2270. recAdd(this.root);
  2271. }
  2272.  
  2273. walkTree(fn) {
  2274. const walk = (tree) => {
  2275. fn(tree);
  2276. tree.list.forEach(walk);
  2277. };
  2278. walk(this.root);
  2279. }
  2280.  
  2281. sort(fn) {
  2282. if (!this.root.elList) return;
  2283. this.walkTree((tree) => {
  2284. tree.list.sort(fn).forEach(subtree => tree.elList.appendChild(subtree.el));
  2285. });
  2286. }
  2287.  
  2288. shuffle() {
  2289. if (!this.root.elList) return;
  2290. const randInt = maximum => Math.floor(Math.random() * (maximum + 1));
  2291. this.walkTree((tree) => {
  2292. const { list } = tree;
  2293. for (let i = 0; i < list.length; i += 1) {
  2294. const j = randInt(i);
  2295. [list[i], list[j]] = [list[j], list[i]];
  2296. }
  2297. list.forEach(subtree => tree.elList.appendChild(subtree.el));
  2298. });
  2299. }
  2300. }
  2301.  
  2302. const commentsTree = new CommentsTree();
  2303. commentsTree.update();
  2304.  
  2305. FLAGS.sortVariants = [
  2306. ['time', 'старые'],
  2307. ];
  2308.  
  2309. if (FLAGS.COMMENTS_SORT_BY_FRESHNESS) FLAGS.sortVariants.push(['freshness', 'новые']);
  2310. if (FLAGS.COMMENTS_SORT_BY_TREND) FLAGS.sortVariants.push(['trend', 'горячие']);
  2311. if (FLAGS.COMMENTS_SORT_BY_QUALITY) FLAGS.sortVariants.push(['quality', 'хорошие']);
  2312. if (FLAGS.COMMENTS_SORT_BY_REDDIT) FLAGS.sortVariants.push(['reddit', 'проверенные']);
  2313. if (FLAGS.COMMENTS_SORT_BY_RATING) FLAGS.sortVariants.push(['rating', 'рейтинговые']);
  2314. if (FLAGS.COMMENTS_SORT_BY_POPULARITY) FLAGS.sortVariants.push(['popularity', 'популярные']);
  2315. if (FLAGS.COMMENTS_SORT_BY_RANDOM) FLAGS.sortVariants.push(['shuffle', 'случайные']);
  2316.  
  2317. // здесь начинается сортировка комментариев
  2318. const commentsOrderEl = document.createElement('div');
  2319. commentsOrderEl.classList.add('comments_order');
  2320. commentsOrderEl.innerHTML = FLAGS.sortVariants.map(([type, text]) => {
  2321. const underline = (type === 'time') ? '; text-decoration: underline' : '';
  2322. return `<a data-order="${type}" style="cursor: pointer${underline}">${text}</a>`;
  2323. }).join(', ');
  2324.  
  2325. if (FLAGS.COMMENTS_SORT && document.getElementById('comments-list')) {
  2326. const commentsList = document.getElementById('comments-list');
  2327. commentsList.parentElement.insertBefore(commentsOrderEl, commentsList);
  2328. }
  2329.  
  2330. const commentsComparators = {
  2331. time(a, b) {
  2332. return a.id - b.id;
  2333. },
  2334.  
  2335. freshness(a, b) {
  2336. return b.id - a.id;
  2337. },
  2338.  
  2339. rating(a, b) {
  2340. const ascore = a.votes.rating;
  2341. const bscore = b.votes.rating;
  2342. if (bscore !== ascore) return bscore - ascore;
  2343. return b.id - a.id;
  2344. },
  2345.  
  2346. popularity(a, b) {
  2347. const aVotes = a.votes.total;
  2348. const bVotes = b.votes.total;
  2349. if (aVotes !== bVotes) return bVotes - aVotes;
  2350. const aLength = a.getLength();
  2351. const bLength = b.getLength();
  2352. if (aLength !== bLength) return bLength - aLength;
  2353. return b.id - a.id;
  2354. },
  2355.  
  2356. quality(a, b) {
  2357. const aQuality = a.votes.rating / a.votes.total || 0;
  2358. const bQuality = b.votes.rating / b.votes.total || 0;
  2359. if (aQuality !== bQuality) return bQuality - aQuality;
  2360. if (a.votes.rating !== b.votes.rating) return b.votes.rating - a.votes.rating;
  2361. return b.id - a.id;
  2362. },
  2363.  
  2364. trend(a, b) {
  2365. // в первые сутки после публикации статьи число посещений больше чем в остальное время
  2366. const oneDay = 24 * 60 * 60 * 1000;
  2367. const firstDayEnd = +datePublication.date + oneDay;
  2368. // у комментария есть только три дня на голосование с момента его создания
  2369. const threeDays = 3 * oneDay;
  2370. const now = Date.now();
  2371.  
  2372. // прикинем число голосов в первый день
  2373. const aDate = +a.date.date;
  2374. let aViews = 0;
  2375. // в первый день
  2376. if (aDate <= firstDayEnd) {
  2377. aViews += Math.min(firstDayEnd, now) - aDate;
  2378. }
  2379. // и в остальное время
  2380. if (now >= firstDayEnd) {
  2381. const threeDaysEnd = aDate + threeDays;
  2382. // для этого соотношения я собрал статистику
  2383. aViews += (Math.min(threeDaysEnd, now) - Math.max(firstDayEnd, aDate)) / 16;
  2384. }
  2385. const aScore = a.votes.rating / aViews;
  2386.  
  2387. // аналогично
  2388. const bDate = +b.date.date;
  2389. let bViews = 0;
  2390. if (bDate <= firstDayEnd) {
  2391. bViews += Math.min(firstDayEnd, now) - bDate;
  2392. }
  2393. if (now >= firstDayEnd) {
  2394. const threeDaysEnd = bDate + threeDays;
  2395. // найти зависимость активности голосования от времени суток не удалось
  2396. bViews += (Math.min(threeDaysEnd, now) - Math.max(firstDayEnd, bDate)) / 16;
  2397. }
  2398. const bScore = b.votes.rating / bViews;
  2399.  
  2400. if (bScore === aScore) return b.id - a.id;
  2401. return bScore - aScore;
  2402. },
  2403.  
  2404. reddit(a, b) {
  2405. const wilsonScore = (ups, downs) => {
  2406. const n = ups + downs;
  2407. if (n === 0) return 0;
  2408. const z = 1.281551565545;
  2409. const p = ups / n;
  2410. const left = p + ((1 / (2 * n)) * z * z);
  2411. const right = z * Math.sqrt(((p * (1 - p)) / n) + ((z * z) / (4 * n * n)));
  2412. const under = 1 + ((1 / n) * (z * z));
  2413. return (left - right) / under;
  2414. };
  2415. const aScore = wilsonScore(a.votes.likes, a.votes.dislikes);
  2416. const bScore = wilsonScore(b.votes.likes, b.votes.dislikes);
  2417. if (bScore === aScore) return b.id - a.id;
  2418. return bScore - aScore;
  2419. },
  2420. };
  2421.  
  2422. const sortComments = () => {
  2423. const order = userConfig.getItem('comments_order');
  2424.  
  2425. Array.from(commentsOrderEl.children).forEach((el) => {
  2426. if (el.dataset.order === order) {
  2427. el.style.textDecoration = 'underline'; // eslint-disable-line no-param-reassign
  2428. } else {
  2429. el.style.textDecoration = ''; // eslint-disable-line no-param-reassign
  2430. }
  2431. });
  2432.  
  2433. if (order === 'shuffle') {
  2434. commentsTree.shuffle();
  2435. } else {
  2436. const compare = commentsComparators[order];
  2437. commentsTree.sort(compare);
  2438. }
  2439. };
  2440.  
  2441. // сортируем комменты при загрузке страницы
  2442. // или не сортируем, если они уже по порядку
  2443. if (FLAGS.COMMENTS_SORT && FLAGS.COMMENTS_SORT_ONLOAD && userConfig.getItem('comments_order') !== 'time') {
  2444. sortComments();
  2445. }
  2446.  
  2447. Array.from(commentsOrderEl.children).forEach((el) => {
  2448. el.onclick = () => { // eslint-disable-line no-param-reassign
  2449. userConfig.setItem('comments_order', el.dataset.order);
  2450. sortComments();
  2451. };
  2452. });
  2453.  
  2454.  
  2455. // меняем ссылки ведущие к новым комментариям на ссылки к началу комментариев
  2456. if (FLAGS.COMMENTS_LINKS) {
  2457. const commentsLinks = document.getElementsByClassName('post-stats__comments-link');
  2458.  
  2459. for (let i = 0; i < commentsLinks.length; i += 1) {
  2460. const iLink = commentsLinks[i];
  2461. const hrefValue = iLink.getAttribute('href');
  2462. const hrefToComments = hrefValue.replace('#first_unread', '#comments');
  2463. iLink.setAttribute('href', hrefToComments);
  2464. }
  2465. }
  2466.  
  2467. // сворачивание комментов
  2468. // eslint-disable-next-line no-constant-condition
  2469. if (false) { // FLAGS.COMMENTS_HIDE
  2470. const commentHash = window.location.hash;
  2471.  
  2472. const toggle = (subtree) => {
  2473. const listLength = subtree.list.length;
  2474. if (listLength === 0) return;
  2475. /* eslint-disable */
  2476. if (subtree.switcherEl.dataset.isVisibleList === 'true') {
  2477. subtree.switcherEl.dataset.isVisibleList = 'false';
  2478. subtree.switcherEl.innerHTML = `\u229E раскрыть ветку ${subtree.getLength()}`;
  2479. subtree.elList.style.display = 'none';
  2480. } else {
  2481. subtree.switcherEl.dataset.isVisibleList = 'true';
  2482. subtree.switcherEl.innerHTML = '\u229F';
  2483. subtree.elList.style.display = 'block';
  2484. }
  2485. /* eslint-enable */
  2486. };
  2487.  
  2488. commentsTree.walkTree((subtree) => {
  2489. // не пытаемся сворачивать корень
  2490. if (subtree.isRoot) return;
  2491. // у похищенных нет футера
  2492. const footerEl = subtree.commentEl.querySelector('.comment__footer');
  2493. if (footerEl === null) return;
  2494. // создаём переключатель
  2495. const switcher = document.createElement('a');
  2496. switcher.classList.add('comment__footer-link');
  2497. switcher.classList.add('comment__switcher');
  2498. switcher.dataset.isVisibleList = 'true';
  2499.  
  2500. switcher.innerHTML = '\u229F';
  2501. if (subtree.list.length === 0) switcher.innerHTML = '\u22A1';
  2502. switcher.style.cursor = 'pointer';
  2503. switcher.style.marginLeft = '-5px';
  2504.  
  2505. footerEl.insertBefore(switcher, footerEl.children[0]);
  2506. subtree.switcherEl = switcher; // eslint-disable-line no-param-reassign
  2507.  
  2508. switcher.onclick = () => toggle(subtree);
  2509.  
  2510. const isHideLvl = subtree.lvl === FLAGS.HIDE_LEVEL && FLAGS.HIDE_LEVEL_4;
  2511. const isLineLvl = subtree.lvl % FLAGS.LINE_LEN === 0;
  2512. if (isLineLvl) {
  2513. subtree.elList.classList.add('comments_new-line');
  2514. const lineNumber = subtree.lvl / FLAGS.LINE_LEN;
  2515. subtree.elList.classList.add(`comments_new-line-${lineNumber % 4}`);
  2516. }
  2517. // при запуске не сворачиваем ветки с новыми комментами, и содержащие целевой id
  2518. if (
  2519. (isHideLvl || isLineLvl) && !subtree.existNew() &&
  2520. !(commentHash && subtree.existId(commentHash))
  2521. ) {
  2522. toggle(subtree);
  2523. }
  2524. });
  2525. }
  2526.  
  2527. if (FLAGS.SCROLL_LEGEND) {
  2528. const postBodyEl = document.querySelector('.post__body_full') || document.querySelector('.article__body');
  2529. const commentsEl = document.getElementById('comments-list');
  2530. const getPercents = (el) => {
  2531. if (!el) return { topPercent: 0, heightPercent: 0 };
  2532. const pageHeight = document.documentElement.scrollHeight;
  2533. const top = el.getBoundingClientRect().top + window.pageYOffset;
  2534. const topPercent = ((100 * top) / pageHeight).toFixed(2);
  2535. const height = el.clientHeight;
  2536. const heightPercent = ((100 * height) / pageHeight).toFixed(2);
  2537.  
  2538. return { topPercent, heightPercent };
  2539. };
  2540.  
  2541. const updateLegend = (pageEl, legendEl) => {
  2542. const { topPercent, heightPercent } = getPercents(pageEl);
  2543. legendEl.style.top = `${topPercent}%`; // eslint-disable-line no-param-reassign
  2544. legendEl.style.height = `${heightPercent}%`; // eslint-disable-line no-param-reassign
  2545. };
  2546.  
  2547. const legendPost = document.createElement('div');
  2548. legendPost.classList.add('legend_el');
  2549. legendPost.style.background = 'rgba(84, 142, 170, 0.66)';
  2550. updateLegend(postBodyEl, legendPost);
  2551. document.body.appendChild(legendPost);
  2552.  
  2553. const legendComments = document.createElement('div');
  2554. legendComments.classList.add('legend_el');
  2555. legendComments.style.background = 'rgba(49, 176, 7, 0.66)';
  2556. updateLegend(commentsEl, legendComments);
  2557. document.body.appendChild(legendComments);
  2558.  
  2559. setInterval(() => {
  2560. updateLegend(postBodyEl, legendPost);
  2561. updateLegend(commentsEl, legendComments);
  2562. }, 1000);
  2563. }
  2564.  
  2565. if (FLAGS.NIGHT_MODE) {
  2566. const switcherEl = document.createElement('div');
  2567. switcherEl.classList.add('night_mode_switcher');
  2568. switcherEl.onclick = () => {
  2569. const isNightMode = userConfig.shiftItem('night_mode');
  2570. document.documentElement.classList.toggle('night', isNightMode);
  2571. };
  2572. document.body.appendChild(switcherEl);
  2573. setInterval(() => {
  2574. const boolClass = document.documentElement.classList.contains('night');
  2575. const isNightMode = userConfig.getItem('night_mode');
  2576. if (boolClass !== isNightMode) {
  2577. document.documentElement.classList.toggle('night', isNightMode);
  2578. }
  2579. }, 1000);
  2580. }
  2581.  
  2582. if (FLAGS.CONFIG_INTERFACE) {
  2583. const configFrame = document.createElement('div');
  2584. configOptions.forEach(([key, text]) => {
  2585. if (typeof FLAGS[key] !== 'boolean') return;
  2586. const inputEl = document.createElement('input');
  2587. inputEl.type = 'checkbox';
  2588. inputEl.value = key;
  2589. inputEl.checked = FLAGS[key];
  2590. const labelEl = document.createElement('label');
  2591. labelEl.setAttribute('unselectable', 'on');
  2592. labelEl.setAttribute('onselectstart', 'return false');
  2593. const spanEl = document.createElement('span');
  2594. spanEl.innerHTML = text;
  2595. configFrame.appendChild(labelEl);
  2596. labelEl.appendChild(inputEl);
  2597. labelEl.appendChild(spanEl);
  2598. inputEl.onchange = () => {
  2599. FLAGS[key] = inputEl.checked;
  2600. localStorage.setItem('habrafixFlags', JSON.stringify(FLAGS));
  2601. };
  2602. configFrame.appendChild(document.createElement('br'));
  2603. });
  2604. const reloadText = document.createElement('div');
  2605. reloadText.style.textAlign = 'right';
  2606. reloadText.innerHTML = `
  2607. * чтобы увидеть изменения
  2608. <a href="#" onclick="location.reload(); return false">
  2609. обновите страницу
  2610. </a>`;
  2611. configFrame.appendChild(reloadText);
  2612. configFrame.classList.add('config_frame');
  2613. configFrame.style.display = 'none';
  2614. document.body.appendChild(configFrame);
  2615.  
  2616. const configButton = document.createElement('div');
  2617. configButton.classList.add('config_button');
  2618. document.body.appendChild(configButton);
  2619.  
  2620. configButton.onclick = () => {
  2621. if (configFrame.style.display) {
  2622. configFrame.style.display = '';
  2623. } else {
  2624. configFrame.style.display = 'none';
  2625. }
  2626. };
  2627. }
  2628.  
  2629. setTimeout(() => {
  2630. const marker = document.createElement('meta');
  2631. marker.id = 'habrafixmarker';
  2632. document.head.appendChild(marker);
  2633. }, 300);
  2634. });