lorify-ng

Юзерскрипт для сайта linux.org.ru поддерживающий загрузку комментариев через технологию WebSocket, а так же уведомления об ответах через системные оповещения.

  1. // ==UserScript==
  2. // @name lorify-ng
  3. // @description Юзерскрипт для сайта linux.org.ru поддерживающий загрузку комментариев через технологию WebSocket, а так же уведомления об ответах через системные оповещения.
  4. // @namespace https://github.com/OpenA
  5. // @include https://www.linux.org.ru/*
  6. // @include http://www.linux.org.ru/*
  7. // @version 2.0.7
  8. // @grant none
  9. // @homepageURL https://www.linux.org.ru/forum/talks/12371302
  10. // @icon https://rawgit.com/OpenA/lorify-ng/master/icons/penguin-32.png
  11. // @run-at document-start
  12. // ==/UserScript==
  13.  
  14. const USER_SETTINGS = {
  15. 'Realtime Loader': true,
  16. 'CSS3 Animation' : true,
  17. 'Delay Open Preview': 0,
  18. 'Delay Close Preview': 800,
  19. 'Desktop Notification': true
  20. }
  21.  
  22. const pagesCache = new Object;
  23. const ResponsesMap = new Object;
  24. const CommentsCache = new Object;
  25. const LoaderSTB = _setup('div', { html: '<div class="page-loader"></div>' });
  26. const LOR = parseLORUrl(location.pathname);
  27. const [,TOKEN = ''] = document.cookie.match(/CSRF_TOKEN="?([^;"]*)/);
  28. const Timer = {
  29. // clear timer by name
  30. clear: function(name) {
  31. clearTimeout(this[name]);
  32. },
  33. // set/replace timer by name
  34. set: function(name, func, t = 50) {
  35. this.clear(name);
  36. this[name] = setTimeout(func, USER_SETTINGS['Delay '+ name] || t);
  37. }
  38. }
  39. document.documentElement.append(
  40. _setup('script', { text: '('+ startRWS.toString() +')(window)', id: 'start-rws'}),
  41. _setup('style' , { text: `
  42. .newadded { border: 1px solid #006880; }
  43. .msg-error { color: red; font-weight: bold; }
  44. .broken { color: inherit !important; cursor: default; }
  45. .response-block, .response-block > a { padding: 0 3px !important; }
  46. .pushed { position: relative; }
  47. .pushed:after {
  48. content: attr(push);
  49. position: absolute;
  50. font-size: 12px;
  51. top: -6px;
  52. color: white;
  53. background: #3d96ab;
  54. line-height: 12px;
  55. padding: 3px;
  56. border-radius: 5px;
  57. }
  58. .deleted > .title:before {
  59. content: "Сообщение удалено";
  60. font-weight: bold;
  61. display: block;
  62. }
  63. .page-loader {
  64. border: 5px solid #f3f3f3;
  65. -webkit-animation: spin 1s linear infinite;
  66. animation: spin 1s linear infinite;
  67. border-top: 5px solid #555;
  68. border-radius: 50%;
  69. width: 50px;
  70. height: 50px;
  71. margin: 500px auto;
  72. }
  73. .terminate {
  74. animation-duration: .4s;
  75. position: relative;
  76. }
  77. .preview {
  78. animation-duration: .3s;
  79. position: absolute;
  80. z-index: 300;
  81. border: 1px solid grey;
  82. }
  83. .slide-down {
  84. max-height: 9999px;
  85. overflow-y: hidden;
  86. animation: slideDown 1.5s ease-in-out;
  87. }
  88. .slide-up {
  89. max-height: 0;
  90. overflow-y: hidden;
  91. animation: slideUp 1s ease-out;
  92. }
  93. @-webkit-keyframes slideDown { from { max-height: 0; } to { max-height: 3000px; } }
  94. @keyframes slideDown { from { max-height: 0; } to { max-height: 3000px; } }
  95. @-webkit-keyframes slideUp { from { max-height: 2000px; } to { max-height: 0; } }
  96. @keyframes slideUp { from { max-height: 2000px; } to { max-height: 0; } }
  97. @-webkit-keyframes toHide { from { opacity: 1; } to { opacity: 0; } }
  98. @keyframes toHide { from { opacity: 1; } to { opacity: 0; } }
  99. @-webkit-keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
  100. @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
  101. @-webkit-keyframes slideToShow { 0% { right: 100%; opacity: 0; } 100% { right: 0%; opacity: 1; } }
  102. @keyframes slideToShow { 0% { right: 100%; opacity: 0; } 100% { right: 0%; opacity: 1; } }
  103. @-webkit-keyframes slideToShow-reverse { 0% { left: 100%; opacity: 0; } 100% { left: 0%; opacity: 1; } }
  104. @keyframes slideToShow-reverse { 0% { left: 100%; opacity: 0; } 100% { left: 0%; opacity: 1; } }
  105. `}));
  106.  
  107. const Navigation = {
  108. pagesCount: 1,
  109. bar: _setup('div', { class: 'nav', html: `
  110. <a class="page-number prev" href="#prev">←</a>
  111. <a class="page-number next" href="#next">→</a>
  112. `, onclick: navBarHandle }),
  113. addToBar: function(pNumEls) {
  114. this.pagesCount = pNumEls.length - 2;
  115. var i = this.bar.children.length - 1;
  116. var pageLinks = '';
  117. for (; i <= this.pagesCount; i++) {
  118. let lp = pNumEls[i].pathname || LOR.path +'#comments';
  119. pageLinks += '\t\t<a id="page_'+ (i - 1) +'" class="page-number" href="'+ lp +'">'+ i +'</a>\n';
  120. }
  121. this.bar.lastElementChild.insertAdjacentHTML('beforebegin', pageLinks);
  122. if (LOR.page === 0) {
  123. this.bar.firstElementChild.classList.add('broken');
  124. this.bar.lastElementChild.href = this.bar.children['page_'+ (LOR.page + 1)].href;
  125. } else
  126. if (LOR.page === this.pagesCount - 1) {
  127. this.bar.lastElementChild.classList.add('broken');
  128. this.bar.firstElementChild.href = this.bar.children['page_'+ (LOR.page - 1)].href;
  129. }
  130. this.bar.children['page_'+ LOR.page].className = 'page-number broken';
  131. return this.bar;
  132. }
  133. }
  134.  
  135. function navBarHandle(e) {
  136. e.target.classList.contains('broken') && e.preventDefault();
  137. }
  138.  
  139. _setup(window, null, {
  140. dblclick: () => {
  141. var newadded = document.querySelectorAll('.newadded');
  142. newadded.forEach(nwc => nwc.classList.remove('newadded'));
  143. Tinycon.setBubble(
  144. (Tinycon.index -= newadded.length)
  145. );
  146. }
  147. });
  148.  
  149. _setup(document, null, {
  150. 'DOMContentLoaded': function onDOMReady() {
  151. this.removeEventListener('DOMContentLoaded', onDOMReady);
  152. this.getElementById('start-rws').remove();
  153. appInit();
  154. if (!LOR.topic) {
  155. return;
  156. }
  157. Tinycon.index = 0;
  158. sessionStorage['rtload'] = +USER_SETTINGS['Realtime Loader'];
  159. const pagesElements = this.querySelectorAll('.messages > .nav > .page-number');
  160. const comments = this.getElementById('comments');
  161. if (pagesElements.length) {
  162. let bar = Navigation.addToBar(pagesElements);
  163. let nav = pagesElements[0].parentNode;
  164. nav.parentNode.replaceChild(bar, nav);
  165. _setup(comments.querySelector('.nav'), { html: bar.innerHTML, onclick: navBarHandle });
  166. }
  167. pagesCache[LOR.page] = comments;
  168. addToCommentsCache( comments.querySelectorAll('.msg[id^="comment-"]') );
  169. },
  170. 'webSocketData': onWSData
  171. });
  172.  
  173. function onWSData({ detail }) {
  174. // Get an HTML containing the comment
  175. fetch(detail.path +'?cid='+ detail[0] +'&skipdeleted=true', { credentials: 'same-origin' }).then(
  176. response => {
  177. if (response.ok) {
  178. const { page } = parseLORUrl(response.url);
  179. const topic = document.getElementById('topic-'+ LOR.topic);
  180. response.text().then(html => {
  181. const comms = getCommentsContent(html);
  182. comms.querySelectorAll('a[itemprop="replyToUrl"]').forEach(a => { a.onclick = toggleForm });
  183. if (page in pagesCache) {
  184. let parent = pagesCache[page];
  185. parent.querySelectorAll('.msg[id^="comment-"]').forEach(msg => {
  186. if (msg.id in comms.children) {
  187. var cand = comms.children[msg.id],
  188. sign = cand.querySelector('.sign_more > time');
  189. if (sign && sign.dateTime !== (msg['last_modifed'] || {}).dateTime) {
  190. msg['last_modifed'] = sign;
  191. msg['edit_comment'] = cand.querySelector('.reply a[href^="/edit_comment"]');
  192. msg['response_block'] && cand.querySelector('.reply > ul')
  193. .appendChild(msg['response_block']);
  194. _setup(cand.querySelector('a[itemprop="replyToUrl"]'), { onclick: toggleForm })
  195. for (var R = msg.children.length; 0 < (R--);) {
  196. parent.replaceChild(cand.children[R], parent.children[R]);
  197. }
  198. } else if (msg['edit_comment']) {
  199. msg['edit_comment'].hidden = !cand.querySelector('.reply a[href^="/edit_comment"]');
  200. }
  201. } else {
  202. _setup(msg, { id: undefined, class: 'msg deleted' });
  203. }
  204. });
  205. for (var i = 0, arr = []; i < detail.length; i++) {
  206. let comment = _setup(comms.children['comment-'+ detail[i]], { class: 'msg newadded' });
  207. if (!comment) {
  208. detail.splice(0, i);
  209. onWSData({ detail });
  210. break;
  211. }
  212. arr.push( parent.appendChild(comment) );
  213. }
  214. Tinycon.index += i;
  215. if (LOR.page !== page) {
  216. let push = i + (
  217. Number ( Navigation.bar.children['page_'+ page].getAttribute('push') ) || 0
  218. );
  219. _setup( Navigation.bar.children['page_'+ page], { class: 'page-number pushed', push: push });
  220. _setup( parent.querySelector('.nav > #page_'+ page), { class: 'page-number pushed', push: push });
  221. }
  222. addToCommentsCache( arr );
  223. } else {
  224. pagesCache[page] = comms;
  225. let nav = comms.querySelector('.nav');
  226. let bar = Navigation.addToBar(nav.children);
  227. let msg = comms.querySelectorAll('.msg[id^="comment-"]');
  228. bar.children['page_'+ page].setAttribute('push', msg.length);
  229. bar.children['page_'+ page].classList.add('pushed');
  230. if (!bar.parentNode) {
  231. let rt = document.getElementById('realtime');
  232. rt.parentNode.insertBefore(bar, rt.nextSibling);
  233. pagesCache[LOR.page].insertBefore(_setup(bar.cloneNode(true), { onclick: navBarHandle }),
  234. pagesCache[LOR.page].firstElementChild.nextSibling);
  235. } else {
  236. _setup(pagesCache[LOR.page].querySelector('.nav'), {
  237. html: bar.innerHTML, onclick: navBarHandle });
  238. }
  239. addToCommentsCache( msg );
  240. Tinycon.index += msg.length;
  241. }
  242. Tinycon.setBubble(Tinycon.index);
  243. history.replaceState(null, document.title, location.pathname);
  244. });
  245. } else {
  246. }
  247. });
  248. }
  249.  
  250. function startRWS(win) {
  251. if ('WebSocket' in win || 'MozWebSocket' in win && (win.WebSocket = MozWebSocket)) {
  252. var timer, detail = new Array(0);
  253. Object.defineProperty(win, 'startRealtimeWS', {
  254. value: function(topic, path, cid, wss) {
  255. var wS = new WebSocket(wss +'ws'),
  256. qA = false;
  257. wS.onmessage = function(e) {
  258. detail.push( (cid = e.data) );
  259. clearTimeout( timer );
  260. timer = setTimeout(function() {
  261. var realtime = document.getElementById('realtime');
  262. if (sessionStorage['rtload'] == '1') {
  263. detail.path = path;
  264. document.dispatchEvent(
  265. new CustomEvent('webSocketData', { detail })
  266. );
  267. detail = new Array(0);
  268. realtime.style.display = 'none';
  269. } else {
  270. realtime.innerHTML = 'Был добавлен новый комментарий.\n<a href="'+
  271. path + '?cid=' + cid +'">Обновить.</a>';
  272. realtime.style.display = null;
  273. }
  274. }, 2e3);
  275. }
  276. wS.onopen = function(e) {
  277. wS.send(topic + (cid == 0 ? '' : ' '+ cid));
  278. }
  279. wS.onclose = function(e) {
  280. setTimeout(function() {
  281. startRealtimeWS(topic, path, cid, wss)
  282. }, 5e3);
  283. }
  284. }
  285. });
  286. }
  287. }
  288.  
  289. function addToCommentsCache(els) {
  290. for (var i = 0; i < els.length; i++) {
  291. let el = els[i],
  292. cid = el.id.replace('comment-', '');
  293. el['last_modifed'] = el.querySelector('.sign_more > time');
  294. el['edit_comment'] = el.querySelector('.reply a[href^="/edit_comment"]');
  295. addPreviewHandler(
  296. (CommentsCache[cid] = el)
  297. );
  298. let acid = el.querySelector('.title > a[href*="cid="]');
  299. if (acid) {
  300. // Extract reply comment ID from the 'search' string
  301. let num = acid.search.match(/cid=(\d+)/)[1];
  302. let url = el.ownerDocument.evaluate('//*[@class="reply"]/ul/li/a[contains(text(), "Ссылка")]/@href',el,null,2,null);
  303. // Write special attributes
  304. _setup(acid, { class: 'link-pref', cid: num });
  305. // Create new response-map for this comment
  306. if (!(num in ResponsesMap)) {
  307. ResponsesMap[num] = new Array(0);
  308. }
  309. ResponsesMap[num].push({
  310. text: (el.querySelector('a[itemprop="creator"]') || { textContent: 'anonymous' }).textContent,
  311. href: url.stringValue,
  312. cid : cid
  313. });
  314. }
  315. }
  316. for (var cid in ResponsesMap) {
  317. if ( cid in CommentsCache ) {
  318. let comment = CommentsCache[cid];
  319. if(!comment['response_block']) {
  320. comment['response_block'] = comment.querySelector('.reply > ul')
  321. .appendChild( _setup('li', { class: 'response-block', text: 'Ответы:' }) );
  322. }
  323. ResponsesMap[cid].forEach(attrs => {
  324. attrs['class' ] = 'link-pref';
  325. attrs['search'] = '?cid='+ attrs.cid;
  326. comment['response_block'].appendChild( _setup('a', attrs) );
  327. });
  328. delete ResponsesMap[cid];
  329. }
  330. }
  331. }
  332.  
  333. function addPreviewHandler(comment) {
  334. comment.addEventListener('mouseover', function(e) {
  335. switch (e.target.classList[0]) {
  336. case 'link-pref':
  337. Timer.clear('Close Preview');
  338. Timer.set('Open Preview', () => showPreview(e));
  339. e.preventDefault();
  340. }
  341. });
  342. comment.addEventListener('mouseout', function(e) {
  343. switch (e.target.classList[0]) {
  344. case 'link-pref':
  345. Timer.clear('Open Preview');
  346. }
  347. });
  348. comment.addEventListener('click', function(e) {
  349. switch (e.target.classList[0]) {
  350. case 'link-pref':
  351. let view = document.getElementById('comment-'+ e.target.getAttribute('cid'));
  352. if (view) {
  353. view.scrollIntoView({ block: 'start', behavior: 'smooth' });
  354. e.preventDefault();
  355. }
  356. }
  357. });
  358. }
  359.  
  360. function getCommentsContent(html) {
  361. // Create new DOM tree
  362. const old = document.getElementById('topic-'+ LOR.topic);
  363. const doc = new DOMParser().parseFromString(html, 'text/html'),
  364. topic = doc.getElementById('topic-'+ LOR.topic),
  365. comms = doc.getElementById('comments');
  366. // Remove banner scripts
  367. comms.querySelectorAll('script').forEach(s => s.remove());
  368. // Replace topic if modifed
  369. if (old.textContent !== topic.textContent) {
  370. tpc.parentNode.replaceChild(topic, old);
  371. topic_memories_form_setup(0, true, LOR.topic, TOKEN);
  372. topic_memories_form_setup(0, false, LOR.topic, TOKEN);
  373. _setup(topic.querySelector('a[href="comment-message.jsp?topic='+ LOR.topic +'"]'), { onclick: toggleForm })
  374. }
  375. return comms;
  376. }
  377.  
  378. function showPreview(e) {
  379. // Get comment's ID from custom attribute
  380. var commentID = e.target.getAttribute('cid'),
  381. commentEl;
  382.  
  383. // Let's reduce an amount of GET requests
  384. // by searching a cache of comments first
  385. if (commentID in CommentsCache) {
  386. commentEl = document.getElementById('preview-'+ commentID);
  387. if (!commentEl) {
  388. // Without the 'clone' call we'll just move the original comment
  389. commentEl = CommentsCache[commentID].cloneNode(
  390. (e.isNew = true)
  391. );
  392. }
  393. } else {
  394. // Add Loading Process stub
  395. commentEl = _setup('article', { class: 'msg preview', text: 'Загрузка...'});
  396. // Get an HTML containing the comment
  397. fetch(e.target.href, { credentials: 'same-origin' }).then(
  398. response => {
  399. if (response.ok) {
  400. const { page } = parseLORUrl(response.url);
  401. response.text().then(html => {
  402. pagesCache[page] = getCommentsContent(html);
  403. addToCommentsCache(
  404. pagesCache[page].querySelectorAll('.msg[id^="comment-"]')
  405. );
  406. if (commentEl.parentNode) {
  407. commentEl.remove();
  408. showCommentInternal(
  409. pagesCache[page].children['comment-'+ commentID].cloneNode((e.isNew = true)),
  410. commentID,
  411. e
  412. );
  413. }
  414. })
  415. } else {
  416. commentEl.textContent = response.status +' '+ response.statusText;
  417. commentEl.classList.add('msg-error');
  418. }
  419. });
  420. }
  421. showCommentInternal(
  422. commentEl,
  423. commentID,
  424. e
  425. );
  426. }
  427.  
  428. const openPreviews = document.getElementsByClassName('preview');
  429.  
  430. function removePreviews(comment) {
  431. var c = openPreviews.length - 1;
  432. while (openPreviews[c] !== comment) {
  433. openPreviews[c--].remove();
  434. }
  435. }
  436.  
  437. function showCommentInternal(commentElement, commentID, e) {
  438. // From makaba
  439. const hoveredLink = e.target;
  440. const parentBlock = document.getElementById('comments');
  441. const { left, top, right, bottom } = hoveredLink.getBoundingClientRect();
  442. const visibleWidth = innerWidth / 2;
  443. const visibleHeight = innerHeight * 0.75;
  444. const offsetX = pageXOffset + left + hoveredLink.offsetWidth / 2;
  445. const offsetY = pageYOffset + bottom + 10;
  446. let postproc = () => {
  447. commentElement.style['left'] = Math.max(
  448. offsetX - (
  449. left < visibleWidth
  450. ? 0
  451. : commentElement.offsetWidth)
  452. , 5) + 'px';
  453. commentElement.style['top'] = pageYOffset + (
  454. top < visibleHeight
  455. ? bottom + 10
  456. : top - commentElement.offsetHeight - 10)
  457. +'px';
  458. if (!USER_SETTINGS['CSS3 Animation'])
  459. commentElement.style['animation-name'] = null;
  460. };
  461. if (e.isNew) {
  462. commentElement.setAttribute(
  463. 'style',
  464. 'animation-name: toShow; '+
  465. // There are no limitations for the 'z-index' in the CSS standard,
  466. // so it depends on the browser. Let's just set it to 300
  467. 'max-width:'+ parentBlock.offsetWidth +
  468. 'px; left: '+
  469. ( left < visibleWidth
  470. ? offsetX
  471. : offsetX - visibleWidth ) +
  472. 'px; top: '+
  473. ( top < visibleHeight
  474. ? offsetY
  475. : 0 ) +'px;'
  476. );
  477. // Avoid duplicated IDs when the original comment was found on the same page
  478. commentElement.id = 'preview-'+ commentID;
  479. commentElement.classList.add('preview');
  480. // If this comment contains link to another comment,
  481. // set the 'mouseover' hook to that 'a' tag
  482. addPreviewHandler( commentElement );
  483. commentElement.addEventListener('animationstart', postproc, true);
  484. } else {
  485. commentElement.style['animation-name'] = null;
  486. postproc();
  487. }
  488. commentElement.onmouseleave = () => {
  489. // remove all preview's
  490. Timer.set('Close Preview', removePreviews)
  491. };
  492. commentElement.onmouseenter = () => {
  493. // remove all preview's after this one
  494. Timer.set('Close Preview', () => removePreviews(commentElement));
  495. };
  496. hoveredLink.onmouseleave = () => {
  497. // remove this preview
  498. Timer.set('Close Preview', () => commentElement.remove());
  499. };
  500. // Note that we append the comment to the '#comments' tag,
  501. // not the document's body
  502. // This is because we want to save the background-color and other styles
  503. // which can be customized by userscripts and themes
  504. parentBlock.appendChild(commentElement);
  505. }
  506.  
  507. function _setup(el, _Attrs, _Events) {
  508. if (el) {
  509. if (typeof el === 'string') {
  510. el = document.createElement(el);
  511. }
  512. if (_Attrs) {
  513. for (var key in _Attrs) {
  514. _Attrs[key] === undefined ? el.removeAttribute(key) :
  515. key === 'html' ? el.innerHTML = _Attrs[key] :
  516. key === 'text' ? el.textContent = _Attrs[key] :
  517. key in el && (el[key] = _Attrs[key] ) == _Attrs[key]
  518. && el[key] == _Attrs[key] || el.setAttribute(key, _Attrs[key]);
  519. }
  520. }
  521. if (_Events) {
  522. if ('remove' in _Events) {
  523. for (var type in _Events['remove']) {
  524. if (_Events['remove'][type].forEach) {
  525. _Events['remove'][type].forEach(function(fn) {
  526. el.removeEventListener(type, fn, false);
  527. });
  528. } else {
  529. el.removeEventListener(type, _Events['remove'][type], false);
  530. }
  531. }
  532. delete _Events['remove'];
  533. }
  534. for (var type in _Events) {
  535. el.addEventListener(type, _Events[type], false);
  536. }
  537. }
  538. }
  539. return el;
  540. }
  541.  
  542. function parseLORUrl(uri) {
  543. const out = new Object;
  544. var m = uri.match(/^(?:https?:\/\/www\.linux\.org\.ru)?(\/\w+\/(?!archive)\w+\/(\d+))(?:\/page(\d+))?/);
  545. if (m) {
  546. out.path = m[1];
  547. out.topic = m[2];
  548. out.page = Number(m[3]) || 0;
  549. }
  550. return out;
  551. }
  552.  
  553. function toggleForm(e) {
  554. const form = document.forms['commentForm'], parent = form.parentNode;
  555. const [, topic, replyto = 0 ] = this.href.match(/jsp\?topic=(\d+)(?:&replyto=(\d+))?$/);
  556. if (!form.elements['csrf'].value) {
  557. form.elements['csrf'].value = TOKEN;
  558. }
  559. if (form.elements['replyto'].value != replyto) {
  560. parent.style['display'] = 'none';
  561. }
  562. if (parent.style['display'] == 'none') {
  563. parent.className = 'slide-down';
  564. parent.addEventListener('animationend', function(e, _) {
  565. _setup(parent, { class: _ }, { remove: { animationend: arguments.callee }});
  566. form.elements['msg'].focus();
  567. });
  568. this.parentNode.parentNode.parentNode.parentNode.appendChild(parent).style['display'] = null;
  569. form.elements['replyto'].value = replyto;
  570. form.elements[ 'topic' ].value = topic;
  571. } else {
  572. parent.className = 'slide-up';
  573. parent.addEventListener('animationend', function(e, _) {
  574. _setup(this, { class: _, style: 'display: none;'}, { remove: { animationend: arguments.callee }});
  575. });
  576. }
  577. e.preventDefault();
  578. }
  579.  
  580. const appInit = (ext => {
  581. if (ext && ext.storage) {
  582. ext.storage.sync.get(USER_SETTINGS, items => {
  583. for (let name in items) {
  584. USER_SETTINGS[name] = items[name];
  585. }
  586. });
  587. ext.storage.onChanged.addListener(items => {
  588. for (let name in items) {
  589. USER_SETTINGS[name] = items[name].newValue;
  590. }
  591. sessionStorage['rtload'] = +USER_SETTINGS['Realtime Loader'];
  592. });
  593. let port = ext.runtime.connect({ name: location.href });
  594. return function() {
  595. var main_events_count = document.getElementById('main_events_count'),
  596. onResponseHandler = main_events_count ? text => {
  597. main_events_count.textContent = text;
  598. } : () => void 0;
  599. // We can't show notification from the content script directly,
  600. // so let's send a corresponding message to the background script
  601. ext.runtime.sendMessage({ action: 'lorify-ng init' }, onResponseHandler);
  602. port.onMessage.addListener(onResponseHandler);
  603. };
  604. } else {
  605. var main_events_count,
  606. sendNotify = () => void 0,
  607. defaults = Object.assign({}, USER_SETTINGS),
  608. delay = 2e4;
  609. start = () => {
  610. const xhr = new XMLHttpRequest;
  611. xhr.open('GET', location.origin +'/notifications-count', true);
  612. xhr.onload = function() {
  613. switch (this.status) {
  614. case 403:
  615. break;
  616. case 200:
  617. var text = '';
  618. if (this.response != '0') {
  619. text = '('+ this.response +')';
  620. if (USER_SETTINGS['Desktop Notification'] && localStorage['notes'] != this.response) {
  621. sendNotify( (localStorage['notes'] = this.response) );
  622. delay = 0;
  623. }
  624. }
  625. main_events_count.textContent = lorynotify.textContent = text;
  626. default:
  627. setTimeout(start, delay < 18e4 ? (delay += 2e4) : delay);
  628. }
  629. }
  630. xhr.send(null);
  631. }
  632. if (localStorage['lorify-ng']) {
  633. let storData = JSON.parse(localStorage.getItem('lorify-ng'));
  634. for (let name in storData) {
  635. USER_SETTINGS[name] = storData[name];
  636. }
  637. }
  638. const onValueChange = function({ target }) {
  639. Timer.clear('Settings on Changed');
  640. switch (target.type) {
  641. case 'checkbox':
  642. USER_SETTINGS[target.id] = target.checked;
  643. break;
  644. case 'number':
  645. USER_SETTINGS[target.id] = target.valueAsNumber >= 0 ? target.valueAsNumber : (target.value = 0);
  646. }
  647. localStorage.setItem('lorify-ng', JSON.stringify(USER_SETTINGS));
  648. applymsg.classList.add('apply-anim');
  649. Timer.set('Apply Setting MSG', () => applymsg.classList.remove('apply-anim'), 2e3);
  650. sessionStorage['rtload'] = +USER_SETTINGS['Realtime Loader'];
  651. }
  652. const loryform = _setup('form', { id: 'loryform', html: `
  653. <div class="tab-row">
  654. <span class="tab-cell">Автоподгрузка комментариев:</span>
  655. <span class="tab-cell" id="applymsg"><input type="checkbox" id="Realtime Loader" ${
  656. USER_SETTINGS['Realtime Loader'] ? 'checked' : '' }></span>
  657. </div>
  658. <div class="tab-row">
  659. <span class="tab-cell">Задержка появления превью:</span>
  660. <span class="tab-cell"><input type="number" id="Delay Open Preview" min="0" step="10" value="${
  661. USER_SETTINGS['Delay Open Preview'] }">
  662. мс
  663. </span>
  664. </div>
  665. <div class="tab-row">
  666. <span class="tab-cell">Задержка исчезания превью:</span>
  667. <span class="tab-cell"><input type="number" id="Delay Close Preview" min="0" step="10" value="${
  668. USER_SETTINGS['Delay Close Preview'] }">
  669. мс
  670. </span>
  671. </div>
  672. <div class="tab-row">
  673. <span class="tab-cell">Оповещения на рабочий стол:</span>
  674. <span class="tab-cell"><input type="checkbox" id="Desktop Notification" ${
  675. USER_SETTINGS['Desktop Notification'] ? 'checked' : '' }>
  676. </span>
  677. </div>
  678. <div class="tab-row">
  679. <span class="tab-cell">CSS анимация:</span>
  680. <span class="tab-cell"><input type="checkbox" id="CSS3 Animation" ${
  681. USER_SETTINGS['CSS3 Animation'] ? 'checked' : '' }>
  682. <input type="button" id="resetSettings" value="сброс" title="вернуть настройки по умолчанию">
  683. </span>
  684. </div>`,
  685. onchange: onValueChange,
  686. oninput: e => Timer.set('Settings on Changed', () => {
  687. loryform.onchange = () => { loryform.onchange = onValueChange };
  688. onValueChange(e)
  689. }, 750)
  690. }),
  691. applymsg = loryform.querySelector('#applymsg');
  692. loryform.elements['resetSettings'].onclick = () => {
  693. for (let name in defaults) {
  694. let inp = loryform.elements[name];
  695. inp[inp.type === 'checkbox' ? 'checked' : 'value'] = (USER_SETTINGS[name] = defaults[name]);
  696. }
  697. localStorage.setItem('lorify-ng', JSON.stringify(USER_SETTINGS));
  698. applymsg.classList.add('apply-anim');
  699. Timer.set('Apply Setting MSG', () => applymsg.classList.remove('apply-anim'), 2e3);
  700. sessionStorage['rtload'] = +USER_SETTINGS['Realtime Loader'];
  701. }
  702. const lorynotify = _setup( 'a' , { id: 'lorynotify', class: 'lory-btn', href: 'notifications' });
  703. const lorytoggle = _setup('div', { id: 'lorytoggle', class: 'lory-btn', html: `<style>
  704. #lorynotify {
  705. right: 60px;
  706. text-decoration: none;
  707. color: inherit;
  708. font: bold 1.2em "Open Sans";
  709. }
  710. #lorytoggle {
  711. width: 32px;
  712. height: 32px;
  713. right: 5px;
  714. cursor: pointer;
  715. opacity: .5;
  716. background: url(//icons.iconarchive.com/icons/icons8/christmas-flat-color/32/penguin-icon.png) center / 100%;
  717. }
  718. #loryform {
  719. display: table;
  720. min-width: 360px;
  721. padding: 3px 6px;
  722. position: fixed;
  723. right: 5px;
  724. top: 40px;
  725. background: #eee;
  726. border-radius: 5px;
  727. }
  728. #lorytoggle:hover, #lorytoggle.pinet { opacity: 1; }
  729. .lory-btn { position: fixed; top: 5px; }
  730. .tab-row { display: table-row; font-size: 85%; color: #666; }
  731. .tab-cell { display: table-cell;
  732. position: relative;
  733. padding: 4px 2px;
  734. vertical-align: middle;
  735. max-width: 180px;
  736. }
  737. #resetSettings, .apply-anim:after { position: absolute; right: 0; }
  738. .apply-anim:after {
  739. content: 'Настройки сохранены.';
  740. -webkit-animation: apply 2s infinite;
  741. animation: apply 2s infinite;
  742. color: red;
  743. }
  744. @keyframes apply { 0% { opacity: .1; } 50% { opacity: 1; } 100% { opacity: 0; } }
  745. @-webkit-keyframes apply { 0% { opacity: .1; } 50% { opacity: 1; } 100% { opacity: 0; } }
  746. </style>`}, {
  747. click: () => { lorytoggle.classList.toggle('pinet') ? document.body.appendChild(loryform) : loryform.remove() }
  748. });
  749. if (Notification.permission === 'granted') {
  750. // Если разрешено то создаем уведомлений
  751. sendNotify = count => new Notification('loryfy-ng', {
  752. icon: '//icons.iconarchive.com/icons/icons8/christmas-flat-color/64/penguin-icon.png',
  753. body: 'Уведомлений: '+ count
  754. }).onclick = () => window.focus();
  755. } else
  756. if (Notification.permission !== 'denied') {
  757. Notification.requestPermission(function(permission) {
  758. // Если пользователь разрешил, то создаем уведомление
  759. if (permission === 'granted') {
  760. sendNotify = count => new Notification('loryfy-ng', {
  761. icon: '//icons.iconarchive.com/icons/icons8/christmas-flat-color/64/penguin-icon.png',
  762. body: 'Уведомлений: '+ count
  763. }).onclick = () => window.focus();
  764. }
  765. });
  766. }
  767. return function() {
  768. if ( (main_events_count = document.getElementById('main_events_count')) ) {
  769. localStorage['notes'] = (
  770. lorynotify.textContent = main_events_count.textContent
  771. ).replace(/\d+/, '$1');
  772. setTimeout(start, delay);
  773. }
  774. document.body.append(lorynotify, lorytoggle);
  775. };
  776. }
  777. })(window.chrome || window.browser);