// ==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);