Habr.Features

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

当前为 2019-04-27 提交的版本,查看 最新版本

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