vk.com mark as read

mark posts as read (помечает посты как прочитанные)

  1. // ==UserScript==
  2. // @name vk.com mark as read
  3. // @namespace limizin.userscripts
  4. // @description mark posts as read (помечает посты как прочитанные)
  5. // @include https://vk.com*
  6. // @version 2.4
  7. // @grant GM_setValue
  8. // @grant GM_getValue
  9. // ==/UserScript==
  10. (function() {
  11. var postwall = document.getElementById('page_wall_posts');
  12. if (!postwall)
  13. return;
  14.  
  15. globals = {};
  16. globals.firefox = navigator.userAgent.toLowerCase().indexOf('firefox') != -1;
  17. globals.postwall = postwall;
  18. globals.storageKey = 'usernameReadPost/' + hashCode(location.pathname) + hashCode(reverse(location.pathname));
  19. globals.top_shadow_post_id = null;
  20. globals.shadowMark = false;
  21. globals.maxAutoscrollPosts = 150;
  22. globals.autoscrolling = false;
  23. globals.scrollOnLoad = false;
  24. globals.unreadCount = 0;
  25. globals.readPostNumId = 0; //second num after _ in id
  26.  
  27. //add styles
  28. if (!document.querySelector('style#usernameReadPost')) {
  29. var head = document.querySelector('head');
  30. stl = head.appendChild(document.createElement('style'));
  31. stl.id = 'usernameReadPost';
  32. stl.innerHTML = `
  33. .usernameReadPost, .usernameReadPost ~ * {background-color: silver !important;}
  34. .usernameReadBtn {background-color: #507299; color: #ffffff; border: thin solid #C4C4C4; cursor: pointer;}
  35. .usernameReadPostBtn {top:0; right:0; position: absolute;}
  36. @keyframes blink {
  37. 0% { color: white; }
  38. 100% { color: #507299; }
  39. }
  40. @-webkit-keyframes blink {
  41. 0% { color: white; }
  42. 100% { color: #507299; }
  43. }
  44. .blink {
  45. -webkit-animation: blink 500ms linear infinite;
  46. -moz-animation: blink 500ms linear infinite;
  47. animation: blink 500ms linear infinite;
  48. }`;
  49. }
  50.  
  51. //scroll section
  52. var buttonBlock = document.createElement('div');
  53. buttonBlock.style.display = 'inline';
  54. buttonBlock.style.marginLeft = '-58px';
  55. buttonBlock.style.marginRight = '7px';
  56. buttonBlock.style.marginTop = '10px';
  57. buttonBlock.style.float = 'left';
  58.  
  59. var scrollOnLoadChBox = buttonBlock.appendChild(document.createElement('input'));
  60. scrollOnLoadChBox.type = 'checkbox';
  61. scrollOnLoadChBox.style.verticalAlign = 'middle';
  62. scrollOnLoadChBox.title = 'прокрутить до прочитанного при загрузке странице';
  63. globals.scrollOnLoadChBox = scrollOnLoadChBox;
  64. scrollOnLoadChBox.click = addEventListener('change', function(e) {
  65. globals.scrollOnLoad = globals.scrollOnLoadChBox.checked;
  66. saveSettings();
  67. });
  68.  
  69. var scrollToReadBtn = createButton('', scrollToRead, 'прокрутить до прочитанного');
  70. scrollToReadBtn.style.width = '30px';
  71. var scrollButtonText = scrollToReadBtn.appendChild(document.createElement('span'));
  72. scrollButtonText.textContent = '>';
  73. globals.scrollButtonText = scrollButtonText;
  74. globals.scrollButton = scrollToReadBtn;
  75.  
  76. buttonBlock.appendChild(scrollToReadBtn);
  77. document.querySelector('div.head_nav_item').appendChild(buttonBlock);
  78.  
  79. //add buttons ol load
  80. _addButtons(document.querySelectorAll('#page_wall_posts > div.post'));
  81.  
  82. loadSettings();
  83.  
  84. //shadow posts on load
  85. if (globals.top_shadow_post_id) {
  86. var post = document.getElementById(globals.top_shadow_post_id);
  87. if (post) {
  88. _markReadPost(post);
  89. globals.shadowMark = true;
  90. }
  91. }
  92.  
  93. //wall observer
  94. observer = new MutationObserver(
  95. function(mutations) {
  96. var newPosts = new Array();
  97. mutations.forEach(function(mutation) {
  98. if (mutation.type != 'childList')
  99. return;
  100. if (mutation.addedNodes) {
  101. for (i = 0; i < mutation.addedNodes.length; i++) {
  102. var post = mutation.addedNodes[i];
  103. if (post.classList.contains('no_posts'))
  104. continue;
  105. newPosts.push(post);
  106. }
  107. }
  108. });
  109.  
  110. if (newPosts) {
  111. _addButtons(newPosts);
  112.  
  113. if (globals.readPostNumId != 0) {
  114. var unreadCount = globals.unreadCount;
  115. //console.log(unreadCount);
  116. for (var i = 0; i < newPosts.length; ++i) {
  117. var newPost = newPosts[i];
  118. var newPostNumId = _extractPostNumId(newPost);
  119. if (newPostNumId > globals.readPostNumId) {
  120. unreadCount += 1;
  121. }
  122. }
  123. if (unreadCount > globals.unreadCount) {
  124. globals.unreadCount = unreadCount;
  125. globals.scrollButtonText.textContent = globals.unreadCount;
  126. }
  127. }
  128. _shadowSubloadedPosts(newPosts);
  129. if (globals.autoscrolling) {
  130. scrollToRead(null);
  131. }
  132. }
  133. }
  134. );
  135. observer.observe(globals.postwall, {
  136. attributes: false,
  137. childList: true,
  138. characterData: false
  139. });
  140.  
  141.  
  142. //scroll on load
  143. if (globals.scrollOnLoad) {
  144. scrollToRead(null);
  145. }
  146.  
  147.  
  148. function loadSettings() {
  149. var value = GM_getValue(globals.storageKey);
  150. if (value) {
  151. var chunks = value.split('|');
  152. globals.top_shadow_post_id = chunks[0];
  153. globals.scrollOnLoad = (chunks[1] == '1');
  154. globals.scrollOnLoadChBox.checked = globals.scrollOnLoad;
  155. }
  156. }
  157.  
  158. function saveSettings() {
  159. var readPostId = globals.top_shadow_post_id;
  160. var checked = (globals.scrollOnLoad) ? '1' : '0';
  161. var value = readPostId + '|' + checked;
  162. GM_setValue(globals.storageKey, value);
  163. }
  164.  
  165. function scrollToRead(event) {
  166. var post = document.querySelector('div.usernameReadPost');
  167. if (post) {
  168. var postYOffset = post.offsetTop;
  169. var median = window.innerHeight / 2;
  170. var scrollTo = postYOffset - median;
  171. if (scrollTo < 0) {
  172. scrollTo = 0;
  173. }
  174. window.scrollTo(0, scrollTo);
  175. changeSrollState(false);
  176. } else {
  177. if (globals.maxAutoscrollPosts >= globals.postwall.childElementCount) {
  178. changeSrollState(true);
  179. var prevPostsBtn = document.getElementById('wall_more_link');
  180. if (prevPostsBtn) {
  181. prevPostsBtn.click();
  182. } else {
  183. changeSrollState(false);
  184. }
  185. } else {
  186. alert(globals.maxAutoscrollPosts + ' постов было загружено. Последний прочитанный пост среди них не найден');
  187. changeSrollState(false);
  188. }
  189. }
  190. }
  191.  
  192. function changeSrollState(start) {
  193. if (start) {
  194. globals.autoscrolling = true;
  195. globals.scrollButtonText.classList.add('blink');
  196. globals.scrollButton.click = null;
  197. } else {
  198. globals.autoscrolling = false;
  199. globals.scrollButtonText.classList.remove('blink');
  200. globals.scrollButton.click = scrollToRead;
  201. }
  202. }
  203.  
  204. function _shadowSubloadedPosts(posts) {
  205. if (globals.shadowMark || !globals.top_shadow_post_id) {
  206. return;
  207. }
  208. for (var i = 0; i < posts.length; ++i) {
  209. var post = posts[i];
  210. var postId = post.getAttribute('id');
  211. if (postId == globals.top_shadow_post_id) {
  212. _markReadPost(post);
  213. break;
  214. }
  215. }
  216. }
  217.  
  218. function markSelectedAsRead(event) {
  219. var xpath = './ancestor::div[contains(@class, "post") and not(@id="page_wall_posts")]';
  220. var post = document.evaluate(xpath, event.currentTarget, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  221. if (post) {
  222. globals.top_shadow_post_id = post.getAttribute('id');
  223. saveSettings();
  224. var prevPostsXpath = './preceding-sibling::div[contains(@class, "usernameReadPost")]';
  225. var prevPosts = document.evaluate(prevPostsXpath, post, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
  226. for (var i = 0; i < prevPosts.snapshotLength; i++) {
  227. var prevPost = prevPosts.snapshotItem(i);
  228. prevPost.classList.remove('usernameReadPost');
  229. }
  230. _markReadPost(post);
  231. globals.shadowMark = true;
  232. }
  233. }
  234.  
  235. function _markReadPost(post) {
  236. post.classList.add('usernameReadPost');
  237. globals.readPostNumId = _extractPostNumId(post);
  238. var prevPostsXpath = 'count(./preceding-sibling::div[contains(@class, "post") and not(@id="page_wall_posts")])';
  239. var prevPosts = document.evaluate(prevPostsXpath, post, null, XPathResult.NUMBER_TYPE, null);
  240. globals.unreadCount = prevPosts.numberValue;
  241. globals.scrollButtonText.textContent = globals.unreadCount;
  242. }
  243.  
  244. function _addButtons(posts) {
  245. for (var i = 0; i < posts.length; ++i) {
  246. var post = posts[i];
  247. var btn = createButton('+', markSelectedAsRead, 'mark as read');
  248. btn.classList.add('usernameReadPostBtn');
  249. post.appendChild(btn);
  250. }
  251. }
  252.  
  253. /////////// utils ///////////
  254.  
  255. function _extractPostNumId(post) {
  256. var num = post.getAttribute('data-post-id').split('_')[1];
  257. return parseInt(num);
  258. }
  259.  
  260. function hashCode(value) {
  261. var hash = 0;
  262. if (value.length == 0) return hash;
  263. for (i = 0; i < value.length; i++) {
  264. char = value.charCodeAt(i);
  265. hash = ((hash << 5) - hash) + char;
  266. hash = hash & hash; // Convert to 32bit integer
  267. }
  268. return hash;
  269. }
  270.  
  271. function reverse(value) {
  272. return value.split('').reverse().join('');
  273. }
  274.  
  275. function xpathResultToArray(xpathResult) {
  276. var nodes = new Array();
  277. var nextNode = xpathResult.iterateNext();
  278. while (nextNode) {
  279. nodes.push(nextNode);
  280. nextNode = xpathResult.iterateNext();
  281. }
  282. return nodes;
  283. }
  284.  
  285. function createButton(content, handler, title) {
  286. var btn = document.createElement('button');
  287. btn.textContent = content;
  288. btn.onclick = handler;
  289. btn.title = title;
  290. btn.className = 'usernameReadBtn';
  291. if (globals.firefox) {
  292. btn.style.paddingBottom = '2px';
  293. }
  294. return btn;
  295. }
  296. })();