您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Юзерскрипт для сайта linux.org.ru поддерживающий загрузку комментариев через технологию WebSocket, а так же уведомления об ответах через системные оповещения.
// ==UserScript== // @name lorify-ng // @description Юзерскрипт для сайта linux.org.ru поддерживающий загрузку комментариев через технологию WebSocket, а так же уведомления об ответах через системные оповещения. // @namespace https://github.com/OpenA // @include https://www.linux.org.ru/* // @include http://www.linux.org.ru/* // @version 2.0.7 // @grant none // @homepageURL https://www.linux.org.ru/forum/talks/12371302 // @icon https://rawgit.com/OpenA/lorify-ng/master/icons/penguin-32.png // @run-at document-start // ==/UserScript== const USER_SETTINGS = { 'Realtime Loader': true, 'CSS3 Animation' : true, 'Delay Open Preview': 0, 'Delay Close Preview': 800, 'Desktop Notification': true } const pagesCache = new Object; const ResponsesMap = new Object; const CommentsCache = new Object; const LoaderSTB = _setup('div', { html: '<div class="page-loader"></div>' }); const LOR = parseLORUrl(location.pathname); const [,TOKEN = ''] = document.cookie.match(/CSRF_TOKEN="?([^;"]*)/); const Timer = { // clear timer by name clear: function(name) { clearTimeout(this[name]); }, // set/replace timer by name set: function(name, func, t = 50) { this.clear(name); this[name] = setTimeout(func, USER_SETTINGS['Delay '+ name] || t); } } document.documentElement.append( _setup('script', { text: '('+ startRWS.toString() +')(window)', id: 'start-rws'}), _setup('style' , { text: ` .newadded { border: 1px solid #006880; } .msg-error { color: red; font-weight: bold; } .broken { color: inherit !important; cursor: default; } .response-block, .response-block > a { padding: 0 3px !important; } .pushed { position: relative; } .pushed:after { content: attr(push); position: absolute; font-size: 12px; top: -6px; color: white; background: #3d96ab; line-height: 12px; padding: 3px; border-radius: 5px; } .deleted > .title:before { content: "Сообщение удалено"; font-weight: bold; display: block; } .page-loader { border: 5px solid #f3f3f3; -webkit-animation: spin 1s linear infinite; animation: spin 1s linear infinite; border-top: 5px solid #555; border-radius: 50%; width: 50px; height: 50px; margin: 500px auto; } .terminate { animation-duration: .4s; position: relative; } .preview { animation-duration: .3s; position: absolute; z-index: 300; border: 1px solid grey; } .slide-down { max-height: 9999px; overflow-y: hidden; animation: slideDown 1.5s ease-in-out; } .slide-up { max-height: 0; overflow-y: hidden; animation: slideUp 1s ease-out; } @-webkit-keyframes slideDown { from { max-height: 0; } to { max-height: 3000px; } } @keyframes slideDown { from { max-height: 0; } to { max-height: 3000px; } } @-webkit-keyframes slideUp { from { max-height: 2000px; } to { max-height: 0; } } @keyframes slideUp { from { max-height: 2000px; } to { max-height: 0; } } @-webkit-keyframes toHide { from { opacity: 1; } to { opacity: 0; } } @keyframes toHide { from { opacity: 1; } to { opacity: 0; } } @-webkit-keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } @-webkit-keyframes slideToShow { 0% { right: 100%; opacity: 0; } 100% { right: 0%; opacity: 1; } } @keyframes slideToShow { 0% { right: 100%; opacity: 0; } 100% { right: 0%; opacity: 1; } } @-webkit-keyframes slideToShow-reverse { 0% { left: 100%; opacity: 0; } 100% { left: 0%; opacity: 1; } } @keyframes slideToShow-reverse { 0% { left: 100%; opacity: 0; } 100% { left: 0%; opacity: 1; } } `})); const Navigation = { pagesCount: 1, bar: _setup('div', { class: 'nav', html: ` <a class="page-number prev" href="#prev">←</a> <a class="page-number next" href="#next">→</a> `, onclick: navBarHandle }), addToBar: function(pNumEls) { this.pagesCount = pNumEls.length - 2; var i = this.bar.children.length - 1; var pageLinks = ''; for (; i <= this.pagesCount; i++) { let lp = pNumEls[i].pathname || LOR.path +'#comments'; pageLinks += '\t\t<a id="page_'+ (i - 1) +'" class="page-number" href="'+ lp +'">'+ i +'</a>\n'; } this.bar.lastElementChild.insertAdjacentHTML('beforebegin', pageLinks); if (LOR.page === 0) { this.bar.firstElementChild.classList.add('broken'); this.bar.lastElementChild.href = this.bar.children['page_'+ (LOR.page + 1)].href; } else if (LOR.page === this.pagesCount - 1) { this.bar.lastElementChild.classList.add('broken'); this.bar.firstElementChild.href = this.bar.children['page_'+ (LOR.page - 1)].href; } this.bar.children['page_'+ LOR.page].className = 'page-number broken'; return this.bar; } } function navBarHandle(e) { e.target.classList.contains('broken') && e.preventDefault(); } _setup(window, null, { dblclick: () => { var newadded = document.querySelectorAll('.newadded'); newadded.forEach(nwc => nwc.classList.remove('newadded')); Tinycon.setBubble( (Tinycon.index -= newadded.length) ); } }); _setup(document, null, { 'DOMContentLoaded': function onDOMReady() { this.removeEventListener('DOMContentLoaded', onDOMReady); this.getElementById('start-rws').remove(); appInit(); if (!LOR.topic) { return; } Tinycon.index = 0; sessionStorage['rtload'] = +USER_SETTINGS['Realtime Loader']; const pagesElements = this.querySelectorAll('.messages > .nav > .page-number'); const comments = this.getElementById('comments'); if (pagesElements.length) { let bar = Navigation.addToBar(pagesElements); let nav = pagesElements[0].parentNode; nav.parentNode.replaceChild(bar, nav); _setup(comments.querySelector('.nav'), { html: bar.innerHTML, onclick: navBarHandle }); } pagesCache[LOR.page] = comments; addToCommentsCache( comments.querySelectorAll('.msg[id^="comment-"]') ); }, 'webSocketData': onWSData }); function onWSData({ detail }) { // Get an HTML containing the comment fetch(detail.path +'?cid='+ detail[0] +'&skipdeleted=true', { credentials: 'same-origin' }).then( response => { if (response.ok) { const { page } = parseLORUrl(response.url); const topic = document.getElementById('topic-'+ LOR.topic); response.text().then(html => { const comms = getCommentsContent(html); comms.querySelectorAll('a[itemprop="replyToUrl"]').forEach(a => { a.onclick = toggleForm }); if (page in pagesCache) { let parent = pagesCache[page]; parent.querySelectorAll('.msg[id^="comment-"]').forEach(msg => { if (msg.id in comms.children) { var cand = comms.children[msg.id], sign = cand.querySelector('.sign_more > time'); if (sign && sign.dateTime !== (msg['last_modifed'] || {}).dateTime) { msg['last_modifed'] = sign; msg['edit_comment'] = cand.querySelector('.reply a[href^="/edit_comment"]'); msg['response_block'] && cand.querySelector('.reply > ul') .appendChild(msg['response_block']); _setup(cand.querySelector('a[itemprop="replyToUrl"]'), { onclick: toggleForm }) for (var R = msg.children.length; 0 < (R--);) { parent.replaceChild(cand.children[R], parent.children[R]); } } else if (msg['edit_comment']) { msg['edit_comment'].hidden = !cand.querySelector('.reply a[href^="/edit_comment"]'); } } else { _setup(msg, { id: undefined, class: 'msg deleted' }); } }); for (var i = 0, arr = []; i < detail.length; i++) { let comment = _setup(comms.children['comment-'+ detail[i]], { class: 'msg newadded' }); if (!comment) { detail.splice(0, i); onWSData({ detail }); break; } arr.push( parent.appendChild(comment) ); } Tinycon.index += i; if (LOR.page !== page) { let push = i + ( Number ( Navigation.bar.children['page_'+ page].getAttribute('push') ) || 0 ); _setup( Navigation.bar.children['page_'+ page], { class: 'page-number pushed', push: push }); _setup( parent.querySelector('.nav > #page_'+ page), { class: 'page-number pushed', push: push }); } addToCommentsCache( arr ); } else { pagesCache[page] = comms; let nav = comms.querySelector('.nav'); let bar = Navigation.addToBar(nav.children); let msg = comms.querySelectorAll('.msg[id^="comment-"]'); bar.children['page_'+ page].setAttribute('push', msg.length); bar.children['page_'+ page].classList.add('pushed'); if (!bar.parentNode) { let rt = document.getElementById('realtime'); rt.parentNode.insertBefore(bar, rt.nextSibling); pagesCache[LOR.page].insertBefore(_setup(bar.cloneNode(true), { onclick: navBarHandle }), pagesCache[LOR.page].firstElementChild.nextSibling); } else { _setup(pagesCache[LOR.page].querySelector('.nav'), { html: bar.innerHTML, onclick: navBarHandle }); } addToCommentsCache( msg ); Tinycon.index += msg.length; } Tinycon.setBubble(Tinycon.index); history.replaceState(null, document.title, location.pathname); }); } else { } }); } function startRWS(win) { if ('WebSocket' in win || 'MozWebSocket' in win && (win.WebSocket = MozWebSocket)) { var timer, detail = new Array(0); Object.defineProperty(win, 'startRealtimeWS', { value: function(topic, path, cid, wss) { var wS = new WebSocket(wss +'ws'), qA = false; wS.onmessage = function(e) { detail.push( (cid = e.data) ); clearTimeout( timer ); timer = setTimeout(function() { var realtime = document.getElementById('realtime'); if (sessionStorage['rtload'] == '1') { detail.path = path; document.dispatchEvent( new CustomEvent('webSocketData', { detail }) ); detail = new Array(0); realtime.style.display = 'none'; } else { realtime.innerHTML = 'Был добавлен новый комментарий.\n<a href="'+ path + '?cid=' + cid +'">Обновить.</a>'; realtime.style.display = null; } }, 2e3); } wS.onopen = function(e) { wS.send(topic + (cid == 0 ? '' : ' '+ cid)); } wS.onclose = function(e) { setTimeout(function() { startRealtimeWS(topic, path, cid, wss) }, 5e3); } } }); } } function addToCommentsCache(els) { for (var i = 0; i < els.length; i++) { let el = els[i], cid = el.id.replace('comment-', ''); el['last_modifed'] = el.querySelector('.sign_more > time'); el['edit_comment'] = el.querySelector('.reply a[href^="/edit_comment"]'); addPreviewHandler( (CommentsCache[cid] = el) ); let acid = el.querySelector('.title > a[href*="cid="]'); if (acid) { // Extract reply comment ID from the 'search' string let num = acid.search.match(/cid=(\d+)/)[1]; let url = el.ownerDocument.evaluate('//*[@class="reply"]/ul/li/a[contains(text(), "Ссылка")]/@href',el,null,2,null); // Write special attributes _setup(acid, { class: 'link-pref', cid: num }); // Create new response-map for this comment if (!(num in ResponsesMap)) { ResponsesMap[num] = new Array(0); } ResponsesMap[num].push({ text: (el.querySelector('a[itemprop="creator"]') || { textContent: 'anonymous' }).textContent, href: url.stringValue, cid : cid }); } } for (var cid in ResponsesMap) { if ( cid in CommentsCache ) { let comment = CommentsCache[cid]; if(!comment['response_block']) { comment['response_block'] = comment.querySelector('.reply > ul') .appendChild( _setup('li', { class: 'response-block', text: 'Ответы:' }) ); } ResponsesMap[cid].forEach(attrs => { attrs['class' ] = 'link-pref'; attrs['search'] = '?cid='+ attrs.cid; comment['response_block'].appendChild( _setup('a', attrs) ); }); delete ResponsesMap[cid]; } } } function addPreviewHandler(comment) { comment.addEventListener('mouseover', function(e) { switch (e.target.classList[0]) { case 'link-pref': Timer.clear('Close Preview'); Timer.set('Open Preview', () => showPreview(e)); e.preventDefault(); } }); comment.addEventListener('mouseout', function(e) { switch (e.target.classList[0]) { case 'link-pref': Timer.clear('Open Preview'); } }); comment.addEventListener('click', function(e) { switch (e.target.classList[0]) { case 'link-pref': let view = document.getElementById('comment-'+ e.target.getAttribute('cid')); if (view) { view.scrollIntoView({ block: 'start', behavior: 'smooth' }); e.preventDefault(); } } }); } function getCommentsContent(html) { // Create new DOM tree const old = document.getElementById('topic-'+ LOR.topic); const doc = new DOMParser().parseFromString(html, 'text/html'), topic = doc.getElementById('topic-'+ LOR.topic), comms = doc.getElementById('comments'); // Remove banner scripts comms.querySelectorAll('script').forEach(s => s.remove()); // Replace topic if modifed if (old.textContent !== topic.textContent) { tpc.parentNode.replaceChild(topic, old); topic_memories_form_setup(0, true, LOR.topic, TOKEN); topic_memories_form_setup(0, false, LOR.topic, TOKEN); _setup(topic.querySelector('a[href="comment-message.jsp?topic='+ LOR.topic +'"]'), { onclick: toggleForm }) } return comms; } function showPreview(e) { // Get comment's ID from custom attribute var commentID = e.target.getAttribute('cid'), commentEl; // Let's reduce an amount of GET requests // by searching a cache of comments first if (commentID in CommentsCache) { commentEl = document.getElementById('preview-'+ commentID); if (!commentEl) { // Without the 'clone' call we'll just move the original comment commentEl = CommentsCache[commentID].cloneNode( (e.isNew = true) ); } } else { // Add Loading Process stub commentEl = _setup('article', { class: 'msg preview', text: 'Загрузка...'}); // Get an HTML containing the comment fetch(e.target.href, { credentials: 'same-origin' }).then( response => { if (response.ok) { const { page } = parseLORUrl(response.url); response.text().then(html => { pagesCache[page] = getCommentsContent(html); addToCommentsCache( pagesCache[page].querySelectorAll('.msg[id^="comment-"]') ); if (commentEl.parentNode) { commentEl.remove(); showCommentInternal( pagesCache[page].children['comment-'+ commentID].cloneNode((e.isNew = true)), commentID, e ); } }) } else { commentEl.textContent = response.status +' '+ response.statusText; commentEl.classList.add('msg-error'); } }); } showCommentInternal( commentEl, commentID, e ); } const openPreviews = document.getElementsByClassName('preview'); function removePreviews(comment) { var c = openPreviews.length - 1; while (openPreviews[c] !== comment) { openPreviews[c--].remove(); } } function showCommentInternal(commentElement, commentID, e) { // From makaba const hoveredLink = e.target; const parentBlock = document.getElementById('comments'); const { left, top, right, bottom } = hoveredLink.getBoundingClientRect(); const visibleWidth = innerWidth / 2; const visibleHeight = innerHeight * 0.75; const offsetX = pageXOffset + left + hoveredLink.offsetWidth / 2; const offsetY = pageYOffset + bottom + 10; let postproc = () => { commentElement.style['left'] = Math.max( offsetX - ( left < visibleWidth ? 0 : commentElement.offsetWidth) , 5) + 'px'; commentElement.style['top'] = pageYOffset + ( top < visibleHeight ? bottom + 10 : top - commentElement.offsetHeight - 10) +'px'; if (!USER_SETTINGS['CSS3 Animation']) commentElement.style['animation-name'] = null; }; if (e.isNew) { commentElement.setAttribute( 'style', 'animation-name: toShow; '+ // There are no limitations for the 'z-index' in the CSS standard, // so it depends on the browser. Let's just set it to 300 'max-width:'+ parentBlock.offsetWidth + 'px; left: '+ ( left < visibleWidth ? offsetX : offsetX - visibleWidth ) + 'px; top: '+ ( top < visibleHeight ? offsetY : 0 ) +'px;' ); // Avoid duplicated IDs when the original comment was found on the same page commentElement.id = 'preview-'+ commentID; commentElement.classList.add('preview'); // If this comment contains link to another comment, // set the 'mouseover' hook to that 'a' tag addPreviewHandler( commentElement ); commentElement.addEventListener('animationstart', postproc, true); } else { commentElement.style['animation-name'] = null; postproc(); } commentElement.onmouseleave = () => { // remove all preview's Timer.set('Close Preview', removePreviews) }; commentElement.onmouseenter = () => { // remove all preview's after this one Timer.set('Close Preview', () => removePreviews(commentElement)); }; hoveredLink.onmouseleave = () => { // remove this preview Timer.set('Close Preview', () => commentElement.remove()); }; // Note that we append the comment to the '#comments' tag, // not the document's body // This is because we want to save the background-color and other styles // which can be customized by userscripts and themes parentBlock.appendChild(commentElement); } function _setup(el, _Attrs, _Events) { if (el) { if (typeof el === 'string') { el = document.createElement(el); } if (_Attrs) { for (var key in _Attrs) { _Attrs[key] === undefined ? el.removeAttribute(key) : key === 'html' ? el.innerHTML = _Attrs[key] : key === 'text' ? el.textContent = _Attrs[key] : key in el && (el[key] = _Attrs[key] ) == _Attrs[key] && el[key] == _Attrs[key] || el.setAttribute(key, _Attrs[key]); } } if (_Events) { if ('remove' in _Events) { for (var type in _Events['remove']) { if (_Events['remove'][type].forEach) { _Events['remove'][type].forEach(function(fn) { el.removeEventListener(type, fn, false); }); } else { el.removeEventListener(type, _Events['remove'][type], false); } } delete _Events['remove']; } for (var type in _Events) { el.addEventListener(type, _Events[type], false); } } } return el; } function parseLORUrl(uri) { const out = new Object; var m = uri.match(/^(?:https?:\/\/www\.linux\.org\.ru)?(\/\w+\/(?!archive)\w+\/(\d+))(?:\/page(\d+))?/); if (m) { out.path = m[1]; out.topic = m[2]; out.page = Number(m[3]) || 0; } return out; } function toggleForm(e) { const form = document.forms['commentForm'], parent = form.parentNode; const [, topic, replyto = 0 ] = this.href.match(/jsp\?topic=(\d+)(?:&replyto=(\d+))?$/); if (!form.elements['csrf'].value) { form.elements['csrf'].value = TOKEN; } if (form.elements['replyto'].value != replyto) { parent.style['display'] = 'none'; } if (parent.style['display'] == 'none') { parent.className = 'slide-down'; parent.addEventListener('animationend', function(e, _) { _setup(parent, { class: _ }, { remove: { animationend: arguments.callee }}); form.elements['msg'].focus(); }); this.parentNode.parentNode.parentNode.parentNode.appendChild(parent).style['display'] = null; form.elements['replyto'].value = replyto; form.elements[ 'topic' ].value = topic; } else { parent.className = 'slide-up'; parent.addEventListener('animationend', function(e, _) { _setup(this, { class: _, style: 'display: none;'}, { remove: { animationend: arguments.callee }}); }); } e.preventDefault(); } const appInit = (ext => { if (ext && ext.storage) { ext.storage.sync.get(USER_SETTINGS, items => { for (let name in items) { USER_SETTINGS[name] = items[name]; } }); ext.storage.onChanged.addListener(items => { for (let name in items) { USER_SETTINGS[name] = items[name].newValue; } sessionStorage['rtload'] = +USER_SETTINGS['Realtime Loader']; }); let port = ext.runtime.connect({ name: location.href }); return function() { var main_events_count = document.getElementById('main_events_count'), onResponseHandler = main_events_count ? text => { main_events_count.textContent = text; } : () => void 0; // We can't show notification from the content script directly, // so let's send a corresponding message to the background script ext.runtime.sendMessage({ action: 'lorify-ng init' }, onResponseHandler); port.onMessage.addListener(onResponseHandler); }; } else { var main_events_count, sendNotify = () => void 0, defaults = Object.assign({}, USER_SETTINGS), delay = 2e4; start = () => { const xhr = new XMLHttpRequest; xhr.open('GET', location.origin +'/notifications-count', true); xhr.onload = function() { switch (this.status) { case 403: break; case 200: var text = ''; if (this.response != '0') { text = '('+ this.response +')'; if (USER_SETTINGS['Desktop Notification'] && localStorage['notes'] != this.response) { sendNotify( (localStorage['notes'] = this.response) ); delay = 0; } } main_events_count.textContent = lorynotify.textContent = text; default: setTimeout(start, delay < 18e4 ? (delay += 2e4) : delay); } } xhr.send(null); } if (localStorage['lorify-ng']) { let storData = JSON.parse(localStorage.getItem('lorify-ng')); for (let name in storData) { USER_SETTINGS[name] = storData[name]; } } const onValueChange = function({ target }) { Timer.clear('Settings on Changed'); switch (target.type) { case 'checkbox': USER_SETTINGS[target.id] = target.checked; break; case 'number': USER_SETTINGS[target.id] = target.valueAsNumber >= 0 ? target.valueAsNumber : (target.value = 0); } localStorage.setItem('lorify-ng', JSON.stringify(USER_SETTINGS)); applymsg.classList.add('apply-anim'); Timer.set('Apply Setting MSG', () => applymsg.classList.remove('apply-anim'), 2e3); sessionStorage['rtload'] = +USER_SETTINGS['Realtime Loader']; } const loryform = _setup('form', { id: 'loryform', html: ` <div class="tab-row"> <span class="tab-cell">Автоподгрузка комментариев:</span> <span class="tab-cell" id="applymsg"><input type="checkbox" id="Realtime Loader" ${ USER_SETTINGS['Realtime Loader'] ? 'checked' : '' }></span> </div> <div class="tab-row"> <span class="tab-cell">Задержка появления превью:</span> <span class="tab-cell"><input type="number" id="Delay Open Preview" min="0" step="10" value="${ USER_SETTINGS['Delay Open Preview'] }"> мс </span> </div> <div class="tab-row"> <span class="tab-cell">Задержка исчезания превью:</span> <span class="tab-cell"><input type="number" id="Delay Close Preview" min="0" step="10" value="${ USER_SETTINGS['Delay Close Preview'] }"> мс </span> </div> <div class="tab-row"> <span class="tab-cell">Оповещения на рабочий стол:</span> <span class="tab-cell"><input type="checkbox" id="Desktop Notification" ${ USER_SETTINGS['Desktop Notification'] ? 'checked' : '' }> </span> </div> <div class="tab-row"> <span class="tab-cell">CSS анимация:</span> <span class="tab-cell"><input type="checkbox" id="CSS3 Animation" ${ USER_SETTINGS['CSS3 Animation'] ? 'checked' : '' }> <input type="button" id="resetSettings" value="сброс" title="вернуть настройки по умолчанию"> </span> </div>`, onchange: onValueChange, oninput: e => Timer.set('Settings on Changed', () => { loryform.onchange = () => { loryform.onchange = onValueChange }; onValueChange(e) }, 750) }), applymsg = loryform.querySelector('#applymsg'); loryform.elements['resetSettings'].onclick = () => { for (let name in defaults) { let inp = loryform.elements[name]; inp[inp.type === 'checkbox' ? 'checked' : 'value'] = (USER_SETTINGS[name] = defaults[name]); } localStorage.setItem('lorify-ng', JSON.stringify(USER_SETTINGS)); applymsg.classList.add('apply-anim'); Timer.set('Apply Setting MSG', () => applymsg.classList.remove('apply-anim'), 2e3); sessionStorage['rtload'] = +USER_SETTINGS['Realtime Loader']; } const lorynotify = _setup( 'a' , { id: 'lorynotify', class: 'lory-btn', href: 'notifications' }); const lorytoggle = _setup('div', { id: 'lorytoggle', class: 'lory-btn', html: `<style> #lorynotify { right: 60px; text-decoration: none; color: inherit; font: bold 1.2em "Open Sans"; } #lorytoggle { width: 32px; height: 32px; right: 5px; cursor: pointer; opacity: .5; background: url(//icons.iconarchive.com/icons/icons8/christmas-flat-color/32/penguin-icon.png) center / 100%; } #loryform { display: table; min-width: 360px; padding: 3px 6px; position: fixed; right: 5px; top: 40px; background: #eee; border-radius: 5px; } #lorytoggle:hover, #lorytoggle.pinet { opacity: 1; } .lory-btn { position: fixed; top: 5px; } .tab-row { display: table-row; font-size: 85%; color: #666; } .tab-cell { display: table-cell; position: relative; padding: 4px 2px; vertical-align: middle; max-width: 180px; } #resetSettings, .apply-anim:after { position: absolute; right: 0; } .apply-anim:after { content: 'Настройки сохранены.'; -webkit-animation: apply 2s infinite; animation: apply 2s infinite; color: red; } @keyframes apply { 0% { opacity: .1; } 50% { opacity: 1; } 100% { opacity: 0; } } @-webkit-keyframes apply { 0% { opacity: .1; } 50% { opacity: 1; } 100% { opacity: 0; } } </style>`}, { click: () => { lorytoggle.classList.toggle('pinet') ? document.body.appendChild(loryform) : loryform.remove() } }); if (Notification.permission === 'granted') { // Если разрешено то создаем уведомлений sendNotify = count => new Notification('loryfy-ng', { icon: '//icons.iconarchive.com/icons/icons8/christmas-flat-color/64/penguin-icon.png', body: 'Уведомлений: '+ count }).onclick = () => window.focus(); } else if (Notification.permission !== 'denied') { Notification.requestPermission(function(permission) { // Если пользователь разрешил, то создаем уведомление if (permission === 'granted') { sendNotify = count => new Notification('loryfy-ng', { icon: '//icons.iconarchive.com/icons/icons8/christmas-flat-color/64/penguin-icon.png', body: 'Уведомлений: '+ count }).onclick = () => window.focus(); } }); } return function() { if ( (main_events_count = document.getElementById('main_events_count')) ) { localStorage['notes'] = ( lorynotify.textContent = main_events_count.textContent ).replace(/\d+/, '$1'); setTimeout(start, delay); } document.body.append(lorynotify, lorytoggle); }; } })(window.chrome || window.browser);