Mark post read on dev.to

Mark post read on dev.to.

  1. // Q:Why I want to write this script?
  2. // A: I often read articles on dev.to site, dev.to provides three buttons, namely like, unicorn and readinglist. But I want a button to indicate whether the current article has been read, similar to the read function of mailing lists and RSS.
  3.  
  4. // ==UserScript==
  5. // @name Mark post read on dev.to
  6. // @namespace http://tampermonkey.net/
  7. // @version 0.1
  8. // @description Mark post read on dev.to.
  9. // @author Du Lin
  10. // @match https://dev.to/*
  11. // @grant none
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/localforage/1.9.0/localforage.min.js
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. const buttonTexts = {
  17. unread: 'Mark As Read',
  18. read: 'Read',
  19. };
  20.  
  21. function bindEvent() {
  22. const widget = document.querySelector('#marked-as-read-menu');
  23. const readButton = document.querySelector('#marked-as-read-button');
  24. const exportButton = document.querySelector('#marked-as-read-export-button');
  25. const closeMenuButton = document.querySelector('#marked-as-read-close-button');
  26. const exportReadListCloseButton = document.querySelector('#marked-as-read-export-read-list-close-button');
  27. readButton.addEventListener('click', onReadButtonClick, false);
  28. widget.addEventListener('click', onWidgetClick, false);
  29. exportButton.addEventListener('click', onExportButtonClick, false);
  30. closeMenuButton.addEventListener('click', onCloseMenuButtonClick, false);
  31. exportReadListCloseButton.addEventListener('click', onExportReadListCloseButtonClick, false);
  32. }
  33.  
  34. async function initStorage() {
  35. localforage.config({
  36. driver: [localforage.INDEXEDDB, localforage.WEBSQL],
  37. name: 'mark-post-read-on-devto',
  38. });
  39. try {
  40. const list = await getReadListFromStorage();
  41. if (!list) {
  42. await localforage.setItem('list', []);
  43. }
  44. } catch (error) {
  45. console.error('intialize error, ', error);
  46. }
  47. }
  48.  
  49. function createReadButton() {
  50. const button = document.createElement('button');
  51. button.id = 'marked-as-read-button';
  52. button.classList.add('crayons-reaction');
  53. button.textContent = buttonTexts.unread;
  54. button.style.cssText = 'border: 1px solid rgb(53, 53, 53);';
  55. return button;
  56. }
  57.  
  58. function createExportButton() {
  59. const button = document.createElement('button');
  60. button.id = 'marked-as-read-export-button';
  61. button.textContent = 'export';
  62. button.style.marginRight = '10px';
  63. return button;
  64. }
  65.  
  66. function createImportButton() {
  67. const button = document.createElement('button');
  68. button.id = 'marked-as-read-import-button';
  69. button.textContent = 'import';
  70. button.style.marginRight = '10px';
  71. return button;
  72. }
  73.  
  74. function createCloseMenuButton() {
  75. const button = document.createElement('button');
  76. button.id = 'marked-as-read-close-button';
  77. button.textContent = 'close';
  78. return button;
  79. }
  80.  
  81. function createExportReadList() {
  82. const div = document.createElement('div');
  83. const title = document.createElement('p');
  84. const closeButton = document.createElement('button');
  85. title.textContent = 'Export Read list';
  86. title.style.marginBottom = '10px';
  87.  
  88. div.id = 'marked-as-read-export-list-container';
  89. div.style.cssText =
  90. 'display: none; z-index: 1000; position: fixed; width: 800px; height: 600px; padding: 20px; background-color: #ddd; top: 0; left: 0;right: 0;bottom: 0;margin: auto';
  91.  
  92. const textarea = document.createElement('textarea');
  93. textarea.id = 'marked-as-read-export-list';
  94. textarea.style.cssText = 'font-size: 14px; resize: none; width: 100%; height: 500px;';
  95.  
  96. closeButton.id = 'marked-as-read-export-read-list-close-button';
  97. closeButton.textContent = 'close';
  98.  
  99. div.appendChild(title);
  100. div.appendChild(textarea);
  101. div.appendChild(closeButton);
  102. return div;
  103. }
  104.  
  105. function createWidget() {
  106. const buttonContainer = document.querySelector('.crayons-article-actions__inner');
  107. const pageContent = document.querySelector('#page-content-inner');
  108. const readButton = createReadButton();
  109.  
  110. const widget = document.createElement('div');
  111. const panel = document.createElement('div');
  112. const widgetHeader = document.createElement('p');
  113. const exportButton = createExportButton();
  114. const importButton = createImportButton();
  115. const closeMenuButton = createCloseMenuButton();
  116. const exportReadList = createExportReadList();
  117.  
  118. widget.id = 'marked-as-read-menu';
  119. widget.style.cssText = [
  120. 'z-index: 100;',
  121. 'position: fixed;',
  122. 'top: 300px;',
  123. 'transform-origin: 25px;',
  124. 'transform: rotate(270deg);',
  125. 'background-color: #ddd;',
  126. 'border: 1px solid #aaa;',
  127. 'padding: 10px;',
  128. 'text-align: center;',
  129. ].join('');
  130.  
  131. widgetHeader.textContent = 'Marked As Read Menu';
  132.  
  133. panel.id = 'marked-as-read-menu-panel';
  134. panel.style.cssText = 'display: none;';
  135.  
  136. panel.appendChild(exportButton);
  137. panel.appendChild(importButton);
  138. panel.appendChild(closeMenuButton);
  139. widget.appendChild(panel);
  140. widget.prepend(widgetHeader);
  141. pageContent.appendChild(widget);
  142. pageContent.appendChild(exportReadList);
  143. buttonContainer.prepend(readButton);
  144.  
  145. return widget;
  146. }
  147.  
  148. function onWidgetClick() {
  149. const panel = document.querySelector('#marked-as-read-menu-panel');
  150. const widget = document.querySelector('#marked-as-read-menu');
  151. panel.style.display = 'block';
  152. widget.style.transform = 'none';
  153. }
  154.  
  155. function onExportReadListCloseButtonClick(e) {
  156. e.stopPropagation();
  157. const readlistWidget = document.querySelector('#marked-as-read-export-list-container');
  158. readlistWidget.style.display = 'none';
  159. }
  160.  
  161. async function onExportButtonClick(e) {
  162. e.stopPropagation();
  163. const readlistWidget = document.querySelector('#marked-as-read-export-list-container');
  164. const readList = document.querySelector('#marked-as-read-export-list');
  165. readlistWidget.style.display = 'block';
  166. const list = await getReadListFromStorage();
  167. readList.value = list.join('\n');
  168. }
  169.  
  170. function onCloseMenuButtonClick(e) {
  171. e.stopPropagation();
  172. const panel = document.querySelector('#marked-as-read-menu-panel');
  173. const widget = document.querySelector('#marked-as-read-menu');
  174. const readlistWidget = document.querySelector('#marked-as-read-export-list-container');
  175. panel.style.display = 'none';
  176. widget.style.transform = 'rotate(270deg)';
  177. readlistWidget.style.display = 'none';
  178. }
  179.  
  180. async function onReadButtonClick(e) {
  181. e.stopPropagation();
  182. const isRead = await checkReadStatus();
  183. if (!isRead) {
  184. const r = AddItemToReadListStorage(window.location.href);
  185. if (r) {
  186. setReadButtonState();
  187. }
  188. } else {
  189. const r = removeItemFromReadListStorage();
  190. if (r) {
  191. resetReadButtonState();
  192. }
  193. }
  194. }
  195.  
  196. async function init() {
  197. createWidget();
  198. bindEvent();
  199. await initStorage();
  200. const isRead = await checkReadStatus();
  201. if (isRead) {
  202. setReadButtonState();
  203. }
  204. }
  205.  
  206. function setReadButtonState() {
  207. const readButton = document.querySelector('#marked-as-read-button');
  208. readButton.textContent = buttonTexts.read;
  209. readButton.classList.add('read');
  210. }
  211.  
  212. function resetReadButtonState() {
  213. const readButton = document.querySelector('#marked-as-read-button');
  214. readButton.textContent = buttonTexts.unread;
  215. readButton.classList.remove('read');
  216. }
  217.  
  218. async function checkReadStatus() {
  219. const list = await getReadListFromStorage();
  220. const idx = list.findIndex((url) => url === window.location.href);
  221. return idx > -1;
  222. }
  223.  
  224. async function AddItemToReadListStorage(item) {
  225. const list = await getReadListFromStorage();
  226. const exists = list.findIndex((url) => url === item) > -1;
  227. if (exists) return false;
  228. list.push(item);
  229. localforage.setItem('list', list);
  230. return true;
  231. }
  232.  
  233. async function removeItemFromReadListStorage(item) {
  234. const list = await getReadListFromStorage();
  235. const idx = list.findIndex((url) => url === item);
  236. list.splice(idx, 1);
  237. localforage.setItem('list', list);
  238. return true;
  239. }
  240.  
  241. function getReadListFromStorage() {
  242. return localforage.getItem('list').catch(console.log);
  243. }
  244.  
  245. init();
  246. })();