您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Всякое-разное для Habr и GeekTimes
当前为
// ==UserScript== // @name Habr.Features // @version 3.0.16 // @description Всякое-разное для Habr и GeekTimes // @author AngReload // @include https://geektimes.com/* // @include https://habr.com/* // @include http://habr.com/* // @include http://geektimes.com/* // @include https://tmfeed.ru // @include http://tmfeed.ru // @namespace habr_comments // @run-at document-start // @grant none // @icon https://habr.com/favicon.ico // ==/UserScript== /* global localStorage, MutationObserver */ // остановка гифок // клик по гифке заменит картинку на заглушку // повторный клик вернет гифку на место const GIF_STOP = true; // остановить гифки при загрузке страницы const GIF_STOP_ONLOAD = false; // цвета заглушки const GIF_STOP_COLOR_FG = 'White'; const GIF_STOP_COLOR_BG = 'LightGray'; // WhiteSmoke // менять src вместо создания-удаления нод const GIF_STOP_OVERTYPE = true; // показывать счетчики рейтинга в виде: // рейтинг = число_голосовавших * (процент_плюсов - процент_минусов)% const RATING_DETAILS = true; // клик мышкой по рейтингу меняет вид на простой \ детальный const RATING_DETAILS_ONCLICK = false; const RATING_DETAILS_PN = false; const KARMA_DETAILS = true; // показывать метки времени в текущем часовом поясе // абсолютно, либо относительно текущего времени, либо относительно родительского времени // меняется по клику, в всплывающей подсказке другие виды времени, автообновляется const TIME_DETAILS = true; // добавить возможность сортировки комментариев const COMMENTS_SORT = true; // сортировать комменты при загрузке страницы или оставить сортировку по времени const COMMENTS_SORT_ONLOAD = true; // список доступных сортировок const sortVariants = [ ['time', 'по времени'], ['freshness', 'свежести'], ['trend', 'трендам'], // ['quality', 'качеству'], // ['rating', 'рейтингу'], // ['popularity', 'популярности'], // ['shuffle', 'перемешать'], ]; // добавить возможность сворачивать комментарии const COMMENTS_HIDE = true; // свернуть комментарии если их глубина вложенности равна некому числу const HIDE_LEVEL = 4; // сделать «возврат каретки» для комментариев чтобы глубина вложенности не превышала некого числа const LINE_LEN = 8; // const LINE_LEN = 16; const REDUCE_NEW_LINES = true; const RAINBOW_NEW_LINE = true; // заменить ссылки ведущие к новым комментариям на дерево комментариев const COMMENTS_LINKS = true; // запоминание галки «Использовать MarkDown» для комментариев const COMMENTS_MD = true; // в огнелисе, в отличии о хрома, при загрузке изображений не сохраняется прокрутка // меня раздражает когда дерево комментариев скачками пытается уехать вниз // эта опция зафиксирует высоту публикации пока та находится вне обзора const FIX_JUMPING_SCROLL = true; const SCROLL_LEGEND = true; // включить разные стили const USERSTYLE = true; const USERSTYLE_GIF_HOVER = false; const USERSTYLE_FEED_DISTANCED = true; const USERSTYLE_COUNTER_NEW_FIX_SPACE = true; const USERSTYLE_REMOVE_SIDEBAR_RIGHT = true; const USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS = true; const USERSTYLE_USERINFO_BIG_AVATARS = true; const USERSTYLE_COMMENTS_IMG_MAXSIZE = 0; const USERSTYLE_COMMENTS_FIX = true; const USERSTYLE_CODE_FIX = true; const USERSTYLE_SPOILER_BORDERS = true; const USERSTYLE_STATIC_STICKY = true; const USERSTYLE_HLJS_LANG = true; const USERSTYLE_HLJS_LANG_HOVER = false; const USERSTYLE_CODE_FONT = 'PT Mono'; const USERSTYLE_CODE_TABSIZE = 2; // свои стили const userStyleEl = document.createElement('style'); let userStyle = ''; if (USERSTYLE_GIF_HOVER) { userStyle += ` .post__text img[src$=".gif"]:hover, .comment__message img[src$=".gif"]:hover { opacity: 0.75; } `; } if (SCROLL_LEGEND) { userStyle += ` .legend_el { position: fixed; width: 4px; right: 0; transition: top 1s ease-out, height 1s ease-out; z-index: 101; } #xpanel { right: 4px; } `; } if (USERSTYLE_FEED_DISTANCED) { userStyle += ` .post__body_crop { text-align: right; } .post__body_crop .post__text { text-align: left; } .post__footer { text-align: right; } .posts_list .content-list__item_post { padding: 40px 0; } `; } if (USERSTYLE_COUNTER_NEW_FIX_SPACE) { userStyle += ` .toggle-menu__item-counter_new { margin-left: 4px; } `; } if (USERSTYLE_REMOVE_SIDEBAR_RIGHT) { // remove for // https://habr.com/post/352896/ // https://habr.com/sandbox/ // https://habr.com/sandbox/115216/ // https://habr.com/users/saggid/posts/ // https://habr.com/users/saggid/comments/ // https://habr.com/users/saggid/favorites/ // https://habr.com/users/saggid/favorites/posts/ // https://habr.com/users/saggid/favorites/comments/ // https://habr.com/company/pvs-studio/blog/353640/ // https://habr.com/company/pvs-studio/blog/ // https://habr.com/company/pvs-studio/blog/top/ // https://habr.com/company/pvs-studio/ // https://habr.com/feed/ // https://habr.com/top/ // https://habr.com/top/yearly/ // https://habr.com/all/ // https://habr.com/all/top10/ // display for // https://habr.com/company/pvs-studio/profile/ // https://habr.com/company/pvs-studio/vacancies/ // https://habr.com/company/pvs-studio/fans/all/rating/ // https://habr.com/company/pvs-studio/workers/new/rating/ // https://habr.com/feed/settings/ // https://habr.com/users/ // https://habr.com/hubs/ // https://habr.com/hubs/admin/ // https://habr.com/companies/ // https://habr.com/companies/category/software/ // https://habr.com/companies/new/ // https://habr.com/flows/design/ const path = window.location.pathname; const isPost = /^\/post\/\d+\/$/.test(path); const isSandbox = /^\/sandbox\//.test(path); const isUserPosts = /^\/users\/[^/]+\/posts\//.test(path); const isUserComments = /^\/users\/[^/]+\/comments\//.test(path); const isUserFavorites = /^\/users\/[^/]+\/favorites\//.test(path); const isCompanyBlog = /^\/company\/[^/]+\/blog\//.test(path); const isCompanyBlog2 = /^\/company\/[^/]+\/(page\d+\/)?$/.test(path); const isFeed = /^\/feed\//.test(path); const isHome = /^\/$/.test(path); const isTop = /^\/top\//.test(path); const isAll = /^\/all\//.test(path); if ( isPost || isSandbox || isUserPosts || isUserComments || isUserFavorites || isCompanyBlog || isCompanyBlog2 || isFeed || isHome || isTop || isAll ) { userStyle += ` .sidebar_right { display: none; } .content_left { padding-right: 0; } .comment_plain { max-width: 860px; } `; } } if (USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS) { userStyle += ` .user-info__image-pic, .user-pic_popover, .media-obj__image-pic { border-radius: 0; } `; } if (USERSTYLE_USERINFO_BIG_AVATARS) { userStyle += ` .page-header { height: auto; } .media-obj__image-pic_hub, .media-obj__image-pic_user, .media-obj__image-pic_company { width: auto; height: auto; } `; } if (USERSTYLE_COMMENTS_FIX) { userStyle += ` .comments_order { color: #333; font-size: 14px; font-family: "-apple-system",BlinkMacSystemFont,Arial,sans-serif; text-rendering: optimizeLegibility; border-bottom: 1px solid #e3e3e3; padding: 8px; text-align: right; } .comments_order a { color: #548eaa; font-style: normal; text-decoration: none; } .comments_order a:hover { color: #487284; } .content-list_comments { overflow: visible; } .comment__folding-dotholder { display: none !important; } .content-list_nested-comments { border-left: 1px solid #e3e3e3; margin: 0; padding-top: 20px; padding-left: 20px !important; } .content-list_comments { /*border-left: 1px solid silver;*/ margin: 0; padding-left: 0; padding-top: 20px; /*background: #FCE4EC;*/ } #comments-list .js-form_placeholder { border-left: 1px solid #e3e3e3; padding-left: 20px; } .comments_new-line { border-left: 1px solid #777; border-bottom: 1px solid #777; border-top: 1px solid #777; margin-left: -${LINE_LEN * 21}px !important; background: white; padding-bottom: 4px; } /* .comment__head_topic-author.comment__head_new-comment */ .comment__head_topic-author .user-info { text-decoration: underline; } `; } if (RAINBOW_NEW_LINE) { userStyle += ` .comments_new-line-1 { border-color: #0caefb; } .comments_new-line-2 { border-color: #06feb7; } .comments_new-line-3 { border-color: #fbcb02; } .comments_new-line-0 { border-color: #fb0543; } `; } if (REDUCE_NEW_LINES) { userStyle += ` .comments_new-line .comments_new-line { margin-left: -${(LINE_LEN - 1) * 21}px !important; } `; } if (USERSTYLE_COMMENTS_IMG_MAXSIZE) { userStyle += ` .comment__message img { max-height: ${USERSTYLE_COMMENTS_IMG_MAXSIZE}px; } .comment__message .spoiler .img { max-height: auto; } `; } if (USERSTYLE_CODE_FIX) { let addFont = ''; if (USERSTYLE_CODE_FONT) { addFont = USERSTYLE_CODE_FONT; if (addFont.indexOf(' ') >= 0) { addFont = `"${addFont}"`; } addFont += ','; } const tabSize = USERSTYLE_CODE_TABSIZE || 4; userStyle += ` .editor .text-holder textarea, .tm-editor__textarea { font-family: ${addFont} Menlo, Monaco, Consolas, 'Lucida Console', 'Courier New', monospace; } code { font-family: ${addFont} Menlo, Monaco, Consolas, 'Lucida Console', 'Courier New', monospace !important;; -o-tab-size: ${tabSize}; -moz-tab-size: ${tabSize}; tab-size: ${tabSize}; background: #f7f7f7; border-radius: 3px; color: #505c66; display: inline-block; font-weight: 500; line-height: 1.29; padding: 5px 9px; vertical-align: 1px; } `; } if (USERSTYLE_SPOILER_BORDERS) { userStyle += ` .spoiler .spoiler_text { border: 1px dashed rgb(12, 174, 251); } `; } if (USERSTYLE_STATIC_STICKY) { userStyle += ` .wrapper-sticky, .js-ad_sticky, .js-ad_sticky_comments { position: static !important; } `; } if (USERSTYLE_HLJS_LANG) { let hover = ''; if (USERSTYLE_HLJS_LANG_HOVER) hover = ':hover'; userStyle += ` pre { position: relative; } .hljs${hover}::after { position: absolute; font-size: 12px; content: 'code'; right: 0; top: 0; padding: 1px 5px 0 4px; /*border-bottom: 1px solid #e5e8ec; border-left: 1px solid #e5e8ec; border-bottom-left-radius: 3px; color: #505c66;*/ opacity: .5; } `; userStyle += [ ['1c', '1C:Enterprise (v7, v8)'], ['abnf', 'Augmented Backus-Naur Form'], ['accesslog', 'Access log'], ['actionscript', 'ActionScript'], ['ada', 'Ada'], ['apache', 'Apache'], ['applescript', 'AppleScript'], ['arduino', 'Arduino'], ['armasm', 'ARM Assembly'], ['asciidoc', 'AsciiDoc'], ['aspectj', 'AspectJ'], ['autohotkey', 'AutoHotkey'], ['autoit', 'AutoIt'], ['avrasm', 'AVR Assembler'], ['awk', 'Awk'], ['axapta', 'Axapta'], ['bash', 'Bash'], ['basic', 'Basic'], ['bnf', 'Backus–Naur Form'], ['brainfuck', 'Brainfuck'], ['cal', 'C/AL'], ['capnproto', 'Cap’n Proto'], ['ceylon', 'Ceylon'], ['clean', 'Clean'], ['clojure-repl', 'Clojure REPL'], ['clojure', 'Clojure'], ['cmake', 'CMake'], ['coffeescript', 'CoffeeScript'], ['coq', 'Coq'], ['cos', 'Caché Object Script'], ['cpp', 'C++'], ['crmsh', 'crmsh'], ['crystal', 'Crystal'], ['cs', 'C#'], ['csp', 'CSP'], ['css', 'CSS'], ['d', 'D'], ['dart', 'Dart'], ['delphi', 'Delphi'], ['diff', 'Diff'], ['django', 'Django'], ['dns', 'DNS Zone file'], ['dockerfile', 'Dockerfile'], ['dos', 'DOS .bat'], ['dsconfig', 'dsconfig'], ['dts', 'Device Tree'], ['dust', 'Dust'], ['ebnf', 'Extended Backus-Naur Form'], ['elixir', 'Elixir'], ['elm', 'Elm'], ['erb', 'ERB (Embedded Ruby)'], ['erlang-repl', 'Erlang REPL'], ['erlang', 'Erlang'], ['excel', 'Excel'], ['fix', 'FIX'], ['flix', 'Flix'], ['fortran', 'Fortran'], ['fsharp', 'F#'], ['gams', 'GAMS'], ['gauss', 'GAUSS'], ['gcode', 'G-code (ISO 6983)'], ['gherkin', 'Gherkin'], ['glsl', 'GLSL'], ['go', 'Go'], ['golo', 'Golo'], ['gradle', 'Gradle'], ['groovy', 'Groovy'], ['haml', 'Haml'], ['handlebars', 'Handlebars'], ['haskell', 'Haskell'], ['haxe', 'Haxe'], ['hsp', 'HSP'], ['htmlbars', 'HTMLBars'], ['http', 'HTTP'], ['hy', 'Hy'], ['inform7', 'Inform 7'], ['ini', 'Ini'], ['irpf90', 'IRPF90'], ['java', 'Java'], ['javascript', 'JavaScript'], ['jboss-cli', 'jboss-cli'], ['json', 'JSON'], ['julia-repl', 'Julia REPL'], ['julia', 'Julia'], ['kotlin', 'Kotlin'], ['lasso', 'Lasso'], ['ldif', 'LDIF'], ['leaf', 'Leaf'], ['less', 'Less'], ['lisp', 'Lisp'], ['livecodeserver', 'LiveCode'], ['livescript', 'LiveScript'], ['llvm', 'LLVM IR'], ['lsl', 'Linden Scripting Language'], ['lua', 'Lua'], ['makefile', 'Makefile'], ['markdown', 'Markdown'], ['mathematica', 'Mathematica'], ['matlab', 'Matlab'], ['maxima', 'Maxima'], ['mel', 'MEL'], ['mercury', 'Mercury'], ['mipsasm', 'MIPS Assembly'], ['mizar', 'Mizar'], ['mojolicious', 'Mojolicious'], ['monkey', 'Monkey'], ['moonscript', 'MoonScript'], ['n1ql', 'N1QL'], ['nginx', 'Nginx'], ['nimrod', 'Nimrod'], ['nix', 'Nix'], ['nsis', 'NSIS'], ['objectivec', 'Objective-C'], ['ocaml', 'OCaml'], ['openscad', 'OpenSCAD'], ['oxygene', 'Oxygene'], ['parser3', 'Parser3'], ['perl', 'Perl'], ['pf', 'pf'], ['php', 'PHP'], ['pony', 'Pony'], ['powershell', 'PowerShell'], ['processing', 'Processing'], ['profile', 'Python profile'], ['prolog', 'Prolog'], ['protobuf', 'Protocol Buffers'], ['puppet', 'Puppet'], ['purebasic', 'PureBASIC'], ['python', 'Python'], ['q', 'Q'], ['qml', 'QML'], ['r', 'R'], ['rib', 'RenderMan RIB'], ['roboconf', 'Roboconf'], ['routeros', 'Microtik RouterOS script'], ['rsl', 'RenderMan RSL'], ['ruby', 'Ruby'], ['ruleslanguage', 'Oracle Rules Language'], ['rust', 'Rust'], ['scala', 'Scala'], ['scheme', 'Scheme'], ['scilab', 'Scilab'], ['scss', 'SCSS'], ['shell', 'Shell Session'], ['smali', 'Smali'], ['smalltalk', 'Smalltalk'], ['sml', 'SML'], ['sqf', 'SQF'], ['sql', 'SQL'], ['stan', 'Stan'], ['stata', 'Stata'], ['step21', 'STEP Part 21'], ['stylus', 'Stylus'], ['subunit', 'SubUnit'], ['swift', 'Swift'], ['taggerscript', 'Tagger Script'], ['tap', 'Test Anything Protocol'], ['tcl', 'Tcl'], ['tex', 'TeX'], ['thrift', 'Thrift'], ['tp', 'TP'], ['twig', 'Twig'], ['typescript', 'TypeScript'], ['vala', 'Vala'], ['vbnet', 'VB.NET'], ['vbscript-html', 'VBScript in HTML'], ['vbscript', 'VBScript'], ['verilog', 'Verilog'], ['vhdl', 'VHDL'], ['vim', 'Vim Script'], ['x86asm', 'Intel x86 Assembly'], ['xl', 'XL'], ['xml', 'HTML, XML'], ['xquery', 'XQuery'], ['yaml', 'YAML'], ['zephir', 'Zephir'], ].map(([langTag, langName]) => `.hljs.${langTag}${hover}::after{content:'${langName} [${langTag}]'}`).join(''); } userStyleEl.innerHTML = userStyle; function readyHead(fn) { if (document.body) { // если есть body, значит head готов fn(); } else if (document.documentElement) { const observer = new MutationObserver(() => { if (document.body) { observer.disconnect(); fn(); } }); observer.observe(document.documentElement, { childList: true }); } else { // рекурсивное ожидание появления DOM setTimeout(() => readyHead(fn), 10); } } readyHead(() => { if (USERSTYLE) document.head.appendChild(userStyleEl); }); function ready(fn) { const { readyState } = document; if (readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { fn(); }); } else { fn(); } } ready(() => { // интерфейс для хранения настроек const userConfig = { // имя записи в localsorage key: 'habrafix', // модель настроек: ключ - возможные значения model: { time_publications: ['fromNow', 'absolute'], time_comments: ['fromParent', 'fromNow', 'absolute'], comments_order: ['trend', 'time'], scores_details: [true, false], comment_markdown: [false, true], }, config: {}, // при старте для конфига берем сохраненные параметры либо по умолчанию init() { let jsonString = localStorage.getItem(userConfig.key); const loadedConfig = jsonString ? JSON.parse(jsonString) : {}; const loadedKeys = Object.keys(loadedConfig); const config = {}; Object.keys(userConfig.model).forEach((key) => { const exist = loadedKeys.indexOf(key) >= 0; config[key] = exist ? loadedConfig[key] : userConfig.model[key][0]; }); jsonString = JSON.stringify(config); localStorage.setItem(userConfig.key, jsonString); userConfig.config = config; }, getItem(key) { return userConfig.config[key]; }, setItem(key, value) { let jsonString = localStorage.getItem(userConfig.key); const config = JSON.parse(jsonString); config[key] = value; jsonString = JSON.stringify(config); localStorage.setItem(userConfig.key, jsonString); userConfig.config = config; }, // каруселит параметр по значения модели shiftItem(key) { const currentValue = userConfig.getItem(key); const availableValues = userConfig.model[key]; const currentIdx = availableValues.indexOf(currentValue); const nextIdx = (currentIdx + 1) % availableValues.length; const nextValue = availableValues[nextIdx]; userConfig.setItem(key, nextValue); return nextValue; }, }; userConfig.init(); if (COMMENTS_MD) { const mdSelectorEl = document.getElementById('comment_markdown'); if (mdSelectorEl) { if (userConfig.getItem('comment_markdown')) mdSelectorEl.checked = true; mdSelectorEl.addEventListener('input', () => { userConfig.setItem('comment_markdown', mdSelectorEl.checked); }); } } // надо ли ещё [...document.querySelectorAll('iframe[src^="https://codepen.io/"]')] .map(el => el.setAttribute('scrolling', 'no')); // остановка гифок по клику и воспроизведение при повторном клике function toggleGIF(el) { // если атрибут со старым линком пуст или отсутствует if (!el.dataset.oldSrc) { // заменим ссылку на data-url-svg с треугольником в круге const w = Math.max(el.clientWidth || 256, 16); const h = Math.max(el.clientHeight || 128, 16); const cx = w / 2; const cy = h / 2; const r = Math.min(w, h) / 4; const ax = (r * 61) / 128; const by = (r * 56) / 128; const bx = (r * 35) / 128; const svg = `data:image/svg+xml;utf8, <svg width='${w}' height='${h}' baseProfile='full' xmlns='http://www.w3.org/2000/svg'> <rect x='0' y='0' width='${w}' height='${h}' fill='${GIF_STOP_COLOR_BG}'/> <circle cx='${cx}' cy='${cy}' r='${r}' fill='${GIF_STOP_COLOR_FG}'/> <polygon points='${cx + ax} ${cy} ${cx - bx} ${cy - by} ${cx - bx} ${cy + by}' fill='${GIF_STOP_COLOR_BG}' /> </svg> `; el.dataset.oldSrc = el.getAttribute('src'); // eslint-disable-line no-param-reassign el.setAttribute('src', svg); } else if (GIF_STOP_OVERTYPE) { // иначе поставим svg с троеточием const w = el.clientWidth; const h = el.clientHeight; const cx = w / 2; const cy = h / 2; const r = Math.min(w, h) / 4; const r2 = r / 4; const svg = `data:image/svg+xml;utf8, <svg width='${w}' height='${h}' baseProfile='full' xmlns='http://www.w3.org/2000/svg'> <rect x='0' y='0' width='${w}' height='${h}' fill='${GIF_STOP_COLOR_BG}'/> <circle cx='${cx - r}' cy='${cy}' r='${r2}' fill='${GIF_STOP_COLOR_FG}'/> <circle cx='${cx}' cy='${cy}' r='${r2}' fill='${GIF_STOP_COLOR_FG}'/> <circle cx='${cx + r}' cy='${cy}' r='${r2}' fill='${GIF_STOP_COLOR_FG}'/> </svg> `; el.setAttribute('src', svg); // когда отрендерится троеточие, можно менять на исходную гифку setTimeout(() => { if (el.dataset.oldSrc) { el.setAttribute('src', el.dataset.oldSrc); el.dataset.oldSrc = ''; // eslint-disable-line no-param-reassign } }, 100); } else { const img = document.createElement('img'); img.setAttribute('src', el.dataset.oldSrc); if (el.hasAttribute('align')) { img.setAttribute('align', el.getAttribute('align')); } el.parentNode.insertBefore(img, el); img.onclick = () => toggleGIF(img); // eslint-disable-line no-param-reassign el.parentNode.removeChild(el); } } if (GIF_STOP) { [...document.querySelectorAll('.post__text img[src$=".gif"], .comment__message img[src$=".gif"]')] .forEach((el) => { if (GIF_STOP_ONLOAD) toggleGIF(el); el.onclick = () => toggleGIF(el); // eslint-disable-line no-param-reassign }); } // фиксирование высоты публикации чтобы убрать прыжки прокрутки if (FIX_JUMPING_SCROLL) { const postBodyEl = document.querySelector('.post__body_full'); const checkPostBodyInViewport = () => postBodyEl.getBoundingClientRect().bottom > 0; const autoHeightPost = () => { if (checkPostBodyInViewport()) { window.removeEventListener('scroll', autoHeightPost); postBodyEl.style.height = 'auto'; } }; if (postBodyEl && !checkPostBodyInViewport()) { const h = postBodyEl.clientHeight; postBodyEl.style.height = `${h}px`; window.addEventListener('scroll', autoHeightPost); } } // счетчики кармы if (KARMA_DETAILS) { Array.from(document.querySelectorAll('.user-info__stats-item.stacked-counter')).forEach((itemCounter) => { itemCounter.style.marginRight = '16px'; // eslint-disable-line no-param-reassign }); Array.from(document.querySelectorAll('.page-header__stats_karma')).forEach((karmaEl) => { karmaEl.style.width = 'auto'; // eslint-disable-line no-param-reassign karmaEl.style.minWidth = '84px'; // eslint-disable-line no-param-reassign }); Array.from(document.querySelectorAll('.stacked-counter[href="/info/help/karma/"]')).forEach((couterEl) => { let total = parseInt(couterEl.title, 10); const scoreEl = couterEl.querySelector('.stacked-counter__value'); if (!scoreEl || !total) return; couterEl.style.width = 'auto'; // eslint-disable-line no-param-reassign couterEl.style.minWidth = '84px'; // eslint-disable-line no-param-reassign const score = parseFloat(scoreEl.innerHTML.replace('–', '-').replace(',', '.'), 10); if (score > total) total = score; const likes = (total + score) / 2; const percent = Math.round((100 * likes) / total); const details = ` = ${total} × (${percent} − ${100 - percent})%`; const detailsEl = document.createElement('span'); detailsEl.innerHTML = details; detailsEl.style.color = '#545454'; detailsEl.style.fontFamily = '"-apple-system",BlinkMacSystemFont,Arial,sans-serif'; detailsEl.style.fontSize = '13px'; detailsEl.style.fontWeight = 'normal'; detailsEl.style.verticalAlign = 'middle'; scoreEl.appendChild(detailsEl); couterEl.title += `, ${(likes).toFixed(2)} плюсов и ${(total - likes).toFixed(2)} минусов`; // eslint-disable-line no-param-reassign }); } // счетчики рейтинга с подробностями const scoresMap = new Map(); class Score { constructor(el) { this.el = el; const data = this.constructor.parse(el); this.rating = data.rating; this.total = data.total; this.likes = data.likes; this.dislikes = data.dislikes; this.isDetailed = false; this.observer = new MutationObserver(() => this.update()); } setDetails(isDetailed) { if (this.isDetailed === isDetailed) return; this.isDetailed = isDetailed; this.update(); } update() { const data = this.constructor.parse(this.el); this.rating = data.rating; this.total = data.total; this.likes = data.likes; this.dislikes = data.dislikes; this.observer.disconnect(); if (this.isDetailed) { this.details(); } else { this.simply(); } this.observer.observe(this.el, { childList: true }); } static parse(el) { let [total, likes, dislikes] = el .attributes.title.textContent .match(/[0-9]+/g).map(Number); let [, sign, rating] = el.innerHTML.match(/([–]?)(\d+)/); // eslint-disable-line prefer-const rating = Number(rating); if (sign) rating = -rating; // не знаю что там происходит при голосовании, так что на всякий случай const diff = rating - (likes - dislikes); if (diff < 0) { total += Math.abs(diff); dislikes += Math.abs(diff); } else if (diff > 0) { total += diff; likes += diff; } return { rating, total, likes, dislikes, }; } simply() { let innerHTML = ''; if (this.rating > 0) { innerHTML = `+${this.rating}`; } else if (this.rating < 0) { innerHTML = `–${Math.abs(this.rating)}`; } else { innerHTML = '0'; } this.el.innerHTML = innerHTML; } details() { let innerHTML = ''; if (this.rating > 0) { innerHTML = `+${this.rating}`; } else if (this.rating < 0) { innerHTML = `–${Math.abs(this.rating)}`; } else { innerHTML = '0'; } if (this.total !== 0) { let details = ''; if (RATING_DETAILS_PN) { details = ` = ${this.likes} − ${this.dislikes}`; } else { const percent = Math.round((100 * this.likes) / this.total); details = ` = ${this.total} × (${percent} − ${100 - percent})%`; } innerHTML += ` <span style='color: #545454; font-weight: normal'>${details}</span>`; } this.el.innerHTML = innerHTML; } } // парсим их [...document.querySelectorAll('.voting-wjt__counter')].forEach((el) => { scoresMap.set(el, new Score(el)); }); // добавляем подробностей if (RATING_DETAILS) { if (RATING_DETAILS_ONCLICK) { const isDetailed = userConfig.getItem('scores_details'); if (isDetailed) scoresMap.forEach(score => score.setDetails(isDetailed)); scoresMap.forEach((score) => { score.el.onclick = () => { // eslint-disable-line no-param-reassign const nowDetailed = userConfig.shiftItem('scores_details'); scoresMap.forEach(s => s.setDetails(nowDetailed)); }; }); } else { scoresMap.forEach(score => score.setDetails(true)); } } // метки времени и работа с ними const pageLoadTime = new Date(); const monthNames = [ 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря', ]; class HabraTime { constructor(el, parent) { this.el = el; this.parent = parent; this.attrDatetime = this.constructor.getAttributeDatetime(el); this.date = new Date(this.attrDatetime); } // вот было бы хорошо, если б на хабре были datetime атрибуты static getAttributeDatetime(el) { const imagination = el.getAttribute('datetime'); if (imagination) return imagination; const re = /((сегодня|вчера)|(\d+)[ .]([а-я]+|\d+)[ .]?(\d+)?) в (\d\d:\d\d)/; let [,, recently, // eslint-disable-line prefer-const day, month, year, time, // eslint-disable-line prefer-const ] = el.innerHTML.match(re); // и местное время let moscow; if (recently || year === undefined) { const offsetMoscow = 3 * 60 * 60 * 1000; const yesterdayShift = (recently === 'вчера') ? 24 * 60 * 60 * 1000 : 0; const offset = pageLoadTime.getTimezoneOffset() * 60 * 1000; const value = (pageLoadTime - yesterdayShift) + offsetMoscow + offset; moscow = new Date(value); } if (recently) { day = moscow.getDate(); month = moscow.getMonth() + 1; } else if (month.length !== 2) { month = monthNames.indexOf(month) + 1; } else { month = +month; } if (day < 10) day = `0${+day}`; if (month < 10) month = `0${month}`; if (year < 100) year = `20${year}`; if (year === undefined) year = moscow.getFullYear(); return `${year}-${month}-${day}T${time}+03:00`; } absolute() { let result = ''; const time = this.date; const day = time.getDate(); const month = time.getMonth(); const monthName = monthNames[month]; const year = time.getFullYear(); const hours = time.getHours(); const minutes = time.getMinutes(); const now = new Date(); const nowDay = now.getDate(); const nowMonth = now.getMonth(); const nowYear = now.getFullYear(); const yesterday = new Date((now - 24) * 60 * 60 * 1000); const yesterdayDay = yesterday.getDate(); const yesterdayMonth = yesterday.getMonth(); const yesterdayYear = yesterday.getFullYear(); const hhmm = `${hours}:${minutes >= 10 ? minutes : `0${minutes}`}`; const isToday = day === nowDay && month === nowMonth && year === nowYear; const isYesterday = day === yesterdayDay && month === yesterdayMonth && year === yesterdayYear; if (isToday) { result = `сегодня в ${hhmm}`; } else if (isYesterday) { result = `вчера в ${hhmm}`; } else if (nowYear === year) { result = `${day} ${monthName} в ${hhmm}`; } else { result = `${day} ${monthName} ${year} в ${hhmm}`; } return result; } static relative(milliseconds) { let result = ''; const pluralForm = (n, forms) => { if (n % 10 === 1 && n % 100 !== 11) return forms[0]; if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return forms[1]; return forms[2]; }; const formats = [ ['год', 'года', 'лет'], ['месяц', 'месяца', 'месяцев'], ['день', 'дня', 'дней'], ['час', 'часа', 'часов'], ['минуту', 'минуты', 'минут'], ]; const minutes = milliseconds / 60000; const hours = minutes / 60; const days = hours / 24; const months = days / 30; const years = months / 12; const idx = [years, months, days, hours, minutes].findIndex(x => x >= 1); if (idx === -1) { result = 'несколько секунд'; } else { const value = Math.floor([years, months, days, hours, minutes][idx]); const forms = formats[idx]; const form = pluralForm(value, forms); result = `${value} ${form}`; } return result; } fromNow() { const diff = Math.abs(Date.now() - this.date); return `${this.constructor.relative(diff)} назад`; } fromParent() { const diff = Math.abs(this.date - this.parent.date); return `через ${this.constructor.relative(diff)}`; } } // собираем метки времени const datesMap = new Map(); const megapostTimeEl = document.querySelector('.megapost-head__meta > .list_inline > .list__item'); (megapostTimeEl ? [megapostTimeEl] : []) .concat([...document.querySelectorAll(` .post__time, .preview-data__time-published, time.comment__date-time_published, .tm-post__date, .user-message__date-time `)]).forEach((el) => { datesMap.set(el, new HabraTime(el)); }); function updateTime() { datesMap.forEach((habraTime) => { let type; let otherTypes; if (habraTime.parent) { type = userConfig.config.time_comments; otherTypes = userConfig.model.time_comments .filter(str => str !== type); } else { type = userConfig.config.time_publications; otherTypes = userConfig.model.time_publications .filter(str => str !== type); } const title = otherTypes.map(otherType => habraTime[otherType]()).join(', '); habraTime.el.innerHTML = habraTime[type](); // eslint-disable-line no-param-reassign habraTime.el.setAttribute('title', title); }); } if (TIME_DETAILS) { datesMap.forEach((habraTime) => { habraTime.el.setAttribute( 'style', 'cursor: pointer; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; user-select: none;', ); habraTime.el.onclick = () => { // eslint-disable-line no-param-reassign if (habraTime.parent) { userConfig.shiftItem('time_comments'); } else { userConfig.shiftItem('time_publications'); } updateTime(); }; }); // подождём, когда дерево комментариев будет построено // у некоторых меток времени будут установлены родители // тогда и обновим их тексты setTimeout(updateTime, 100); setInterval(updateTime, 30 * 1000); } // время публикации, понадобится для корня древа комментариев let datePublication = datesMap.get(megapostTimeEl || document.querySelector('.post__time')); // если нету публикации поищем самую раннюю метку времени if (!datePublication) { datePublication = { date: pageLoadTime }; datesMap.forEach((date) => { if (date.date < datePublication.date) datePublication = date; }); } // создаем дерево комментариев class ItemComment { constructor(el, parent) { this.parent = parent; this.el = el; this.lvl = parent.lvl + 1; this.id = Number(el.getAttribute('rel')); this.commentEl = el.querySelector('.comment'); if (this.commentEl) { this.timeEl = this.commentEl.querySelector('time'); this.ratingEl = this.commentEl.querySelector('.js-score'); } this.date = datesMap.get(this.timeEl); if (this.date) { this.date.parent = parent.date; } else { this.date = parent.date; } this.votes = scoresMap.get(this.ratingEl) || { total: 0, likes: 0, dislikes: 0, rating: 0, }; this.elList = el.querySelector('.content-list_nested-comments'); } existId(id) { return !!this.elList.querySelector(id); } existNew() { return !!this.elList.querySelector('.js-comment_new'); } getLength() { let { length } = this.list; this.list.forEach((node) => { length += node.getLength(); }); return length; } } class CommentsTree { constructor() { this.root = { isRoot: true, date: datePublication, lvl: 0, elList: document.getElementById('comments-list'), list: [], }; } static exist() { return !!document.getElementById('comments-list'); } update() { if (!this.root.elList) return; const recAdd = (node) => { node.list = Array.from(node.elList.children) // eslint-disable-line no-param-reassign .map(el => new ItemComment(el, node)); node.list.forEach(recAdd); }; recAdd(this.root); } walkTree(fn) { const walk = (tree) => { fn(tree); tree.list.forEach(walk); }; walk(this.root); } sort(fn) { if (!this.root.elList) return; this.walkTree((tree) => { tree.list.sort(fn).forEach(subtree => tree.elList.appendChild(subtree.el)); }); } shuffle() { if (!this.root.elList) return; const randInt = maximum => Math.floor(Math.random() * (maximum + 1)); this.walkTree((tree) => { const { list } = tree; for (let i = 0; i < list.length; i += 1) { const j = randInt(i); [list[i], list[j]] = [list[j], list[i]]; } list.forEach(subtree => tree.elList.appendChild(subtree.el)); }); } } const commentsTree = new CommentsTree(); commentsTree.update(); // здесь начинается сортировка комментариев const commentsOrderEl = document.createElement('div'); commentsOrderEl.classList.add('comments_order'); commentsOrderEl.innerHTML = sortVariants.map(([type, text]) => { const underline = (type === 'time') ? 'style="text-decoration: underline;"' : ''; return `<a href='#' data-order="${type}" ${underline}>${text}</a>`; }).join(', '); if (COMMENTS_SORT && document.getElementById('comments-list')) { const commentsList = document.getElementById('comments-list'); commentsList.parentElement.insertBefore(commentsOrderEl, commentsList); } const commentsComparators = { time(a, b) { return a.id - b.id; }, freshness(a, b) { return b.id - a.id; }, rating(a, b) { const ascore = a.votes.rating; const bscore = b.votes.rating; if (bscore !== ascore) return bscore - ascore; return b.id - a.id; }, popularity(a, b) { const aVotes = a.votes.total; const bVotes = b.votes.total; if (aVotes !== bVotes) return bVotes - aVotes; const aLength = a.getLength(); const bLength = b.getLength(); if (aLength !== bLength) return bLength - aLength; return b.id - a.id; }, quality(a, b) { const aQuality = a.votes.rating / a.votes.total || 0; const bQuality = b.votes.rating / b.votes.total || 0; if (aQuality !== bQuality) return bQuality - aQuality; if (a.votes.rating !== b.votes.rating) return b.votes.rating - a.votes.rating; return b.id - a.id; }, trend(a, b) { // в первые сутки после публикации статьи число посещений больше чем в остальное время const oneDay = 24 * 60 * 60 * 1000; const firstDayEnd = +datePublication.date + oneDay; // у комментария есть только три дня на голосование с момента его создания const threeDays = 3 * oneDay; const now = Date.now(); // прикинем число голосов в первый день const aDate = +a.date.date; let aViews = 0; // в первый день if (aDate <= firstDayEnd) { aViews += Math.min(firstDayEnd, now) - aDate; } // и в остальное время if (now >= firstDayEnd) { const threeDaysEnd = aDate + threeDays; // для этого соотношения я собрал статистику aViews += (Math.min(threeDaysEnd, now) - Math.max(firstDayEnd, aDate)) / 16; } const aScore = a.votes.rating / aViews; // аналогично const bDate = +b.date.date; let bViews = 0; if (bDate <= firstDayEnd) { bViews += Math.min(firstDayEnd, now) - bDate; } if (now >= firstDayEnd) { const threeDaysEnd = bDate + threeDays; // найти зависимость активности голосования от времени суток не удалось bViews += (Math.min(threeDaysEnd, now) - Math.max(firstDayEnd, bDate)) / 16; } const bScore = b.votes.rating / bViews; if (bScore === aScore) return b.id - a.id; return bScore - aScore; }, }; const sortComments = () => { const order = userConfig.getItem('comments_order'); Array.from(commentsOrderEl.children).forEach((el) => { if (el.dataset.order === order) { el.style.textDecoration = 'underline'; // eslint-disable-line no-param-reassign } else { el.style.textDecoration = ''; // eslint-disable-line no-param-reassign } }); if (order === 'shuffle') { commentsTree.shuffle(); } else { const compare = commentsComparators[order]; commentsTree.sort(compare); } }; // сортируем комменты при загрузке страницы // или не сортируем, если они уже по порядку if (COMMENTS_SORT && COMMENTS_SORT_ONLOAD && userConfig.getItem('comments_order') !== 'time') { sortComments(); } Array.from(commentsOrderEl.children).forEach((el) => { el.onclick = () => { // eslint-disable-line no-param-reassign userConfig.setItem('comments_order', el.dataset.order); sortComments(); }; }); // меняем ссылки ведущие к новым комментариям на ссылки к началу комментариев if (COMMENTS_LINKS) { const commentsLinks = document.getElementsByClassName('post-stats__comments-link'); for (let i = 0; i < commentsLinks.length; i += 1) { const iLink = commentsLinks[i]; const hrefValue = iLink.getAttribute('href'); const hrefToComments = hrefValue.replace('#first_unread', '#comments'); iLink.setAttribute('href', hrefToComments); } } // сворачивание комментов if (COMMENTS_HIDE) { const commentHash = window.location.hash; const toggle = (subtree) => { const listLength = subtree.list.length; if (listLength === 0) return; /* eslint-disable */ if (subtree.switcherEl.dataset.isVisibleList === 'true') { subtree.switcherEl.dataset.isVisibleList = 'false'; subtree.switcherEl.innerHTML = `\u229E раскрыть ветвь ${subtree.getLength()}`; subtree.elList.style.display = 'none'; } else { subtree.switcherEl.dataset.isVisibleList = 'true'; subtree.switcherEl.innerHTML = '\u229F'; subtree.elList.style.display = 'block'; } /* eslint-enable */ }; const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') >= 0; commentsTree.walkTree((subtree) => { // не пытаемся сворачивать корень if (subtree.isRoot) return; // у похищенных нет футера const footerEl = subtree.commentEl.querySelector('.comment__footer'); if (footerEl === null) return; // создаём переключатель const switcher = document.createElement('a'); switcher.setAttribute('href', '#'); switcher.classList.add('comment__footer-link'); switcher.classList.add('comment__switcher'); switcher.dataset.isVisibleList = 'true'; switcher.innerHTML = '\u229F'; if (subtree.list.length === 0) switcher.innerHTML = '\u22A1'; switcher.style.marginLeft = isFirefox ? '-4px' : '-6px'; footerEl.insertBefore(switcher, footerEl.children[0]); subtree.switcherEl = switcher; // eslint-disable-line no-param-reassign switcher.onclick = () => toggle(subtree); const isHideLvl = subtree.lvl === HIDE_LEVEL; const isLineLvl = subtree.lvl % LINE_LEN === 0; if (isLineLvl) { subtree.elList.classList.add('comments_new-line'); const lineNumber = subtree.lvl / LINE_LEN; subtree.elList.classList.add(`comments_new-line-${lineNumber % 4}`); } // при запуске не сворачиваем ветки с новыми комментами, и содержащие целевой id if ( (isHideLvl || isLineLvl) && !subtree.existNew() && !(commentHash && subtree.existId(commentHash)) ) { toggle(subtree); } }); } if (SCROLL_LEGEND) { const postBodyEl = document.querySelector('.post__body_full') || document.querySelector('.article__body'); const commentsEl = document.getElementById('comments-list'); const getPercents = (el) => { if (!el) return { topPercent: 0, heightPercent: 0 }; const pageHeight = document.documentElement.scrollHeight; const top = el.getBoundingClientRect().top + window.pageYOffset; const topPercent = ((100 * top) / pageHeight).toFixed(2); const height = el.clientHeight; const heightPercent = ((100 * height) / pageHeight).toFixed(2); return { topPercent, heightPercent }; }; const updateLegend = (pageEl, legendEl) => { const { topPercent, heightPercent } = getPercents(pageEl); legendEl.style.top = `${topPercent}%`; // eslint-disable-line no-param-reassign legendEl.style.height = `${heightPercent}%`; // eslint-disable-line no-param-reassign }; const legendPost = document.createElement('div'); legendPost.classList.add('legend_el'); legendPost.style.background = 'rgba(84, 142, 170, 0.66)'; updateLegend(postBodyEl, legendPost); document.body.appendChild(legendPost); const legendComments = document.createElement('div'); legendComments.classList.add('legend_el'); legendComments.style.background = 'rgba(49, 176, 7, 0.66)'; updateLegend(commentsEl, legendComments); document.body.appendChild(legendComments); setInterval(() => { updateLegend(postBodyEl, legendPost); updateLegend(commentsEl, legendComments); }, 1000); } });