DTF Enhancer

Выводит список подписок в сайдбаре и раскрывает список комментов

目前为 2024-03-14 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name DTF Enhancer
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.0.5
  5. // @description Выводит список подписок в сайдбаре и раскрывает список комментов
  6. // @author You
  7. // @match *://dtf.ru/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=dtf.ru
  9. // @run-at document-end
  10. // @grant GM.getValue
  11. // @grant GM.setValue
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. const dict = {"Ё":"YO","Й":"I","Ц":"TS","У":"U","К":"K","Е":"E","Н":"N","Г":"G","Ш":"SH","Щ":"SCH","З":"Z","Х":"H","Ъ":"'","ё":"yo","й":"i","ц":"ts","у":"u","к":"k","е":"e","н":"n","г":"g","ш":"sh","щ":"sch","з":"z","х":"h","ъ":"'","Ф":"F","Ы":"I","В":"V","А":"A","П":"P","Р":"R","О":"O","Л":"L","Д":"D","Ж":"ZH","Э":"E","ф":"f","ы":"i","в":"v","а":"a","п":"p","р":"r","о":"o","л":"l","д":"d","ж":"zh","э":"e","Я":"Ya","Ч":"CH","С":"S","М":"M","И":"I","Т":"T","Ь":"'","Б":"B","Ю":"YU","я":"ya","ч":"ch","с":"s","м":"m","и":"i","т":"t","ь":"'","б":"b","ю":"yu"};
  17.  
  18. const transliterate = (word) => word.split('').map((char) => dict[char] || char).join("");
  19.  
  20. const cn = (tagName, attrs = {}, childrenList = [], parentNode = null) => {
  21. const node = document.createElement(tagName);
  22.  
  23. if (typeof attrs === 'object') {
  24. for (const attrsKey in attrs) node.setAttribute(attrsKey, attrs[attrsKey]);
  25. }
  26.  
  27. if (Array.isArray(childrenList)) {
  28. childrenList.forEach(child => {
  29. node.appendChild(typeof child === 'string' ? document.createTextNode(child) : child);
  30. });
  31. }
  32.  
  33. if (parentNode) {
  34. parentNode.appendChild(node);
  35. }
  36.  
  37. return node;
  38. };
  39.  
  40. const getDomElementAsync = (selector, timerLimit = 10000) => {
  41. return new Promise((resolve, reject) => {
  42. try {
  43. setTimeout(() => {
  44. console.log(`Время ожидания DOM элемента ${selector} истекло (${timerLimit / 1000}s)`);
  45. resolve(null);
  46. }, timerLimit);
  47.  
  48. let timerId;
  49.  
  50. const tick = () => {
  51. const element = document.querySelector(selector);
  52.  
  53. if (element) {
  54. clearTimeout(timerId);
  55. resolve(element);
  56. } else {
  57. timerId = setTimeout(tick, 100);
  58. }
  59. };
  60.  
  61. tick();
  62. } catch (e) {
  63. reject(e);
  64. }
  65. });
  66. };
  67.  
  68. const debounce = (func, wait) => {
  69. let timeout;
  70. return function (...args) {
  71. return new Promise(resolve => {
  72. clearTimeout(timeout);
  73. timeout = setTimeout(() => {
  74. timeout = null;
  75. Promise.resolve(func.apply(this, [...args])).then(resolve);
  76. }, wait);
  77. });
  78. };
  79. };
  80.  
  81. const observeUrlChange = async (onChange) => {
  82. await GM.setValue('currentUrl', window.location.href);
  83.  
  84. const onChangeHandler = async () => {
  85. const oldHref = await GM.getValue('currentUrl');
  86. const newHref = window.location.href;
  87.  
  88. if (oldHref !== newHref) {
  89. console.log('observeUrlChange');
  90.  
  91. await GM.setValue('currentUrl', newHref);
  92. onChange?.();
  93. }
  94. };
  95.  
  96. const debouncedOnChangeHandler = debounce(onChangeHandler, 500);
  97.  
  98. const observer = new MutationObserver(debouncedOnChangeHandler);
  99.  
  100. observer.observe(document.body, {
  101. childList: true,
  102. subtree: true,
  103. });
  104. };
  105.  
  106. const injectStyles = () => {
  107. const styles = `
  108. .sidebar-subs {
  109. display: flex;
  110. flex-direction: column;
  111. overflow: auto;
  112. margin: 24px 0;
  113. }
  114.  
  115. .sidebar-sibs__title {
  116. margin-bottom: 16px;
  117. padding: 0 8px;
  118. font-weight: 500;
  119. font-size: 18px;
  120. }
  121.  
  122. .sidebar-sibs__list {
  123. padding: 0;
  124. }
  125.  
  126. .sidebar-item._sub img.icon {
  127. width: 24px;
  128. border-radius: 50%;
  129. }
  130.  
  131. .sidebar-item._sub span {
  132. white-space: nowrap;
  133. overflow: hidden;
  134. text-overflow: ellipsis;
  135. min-width: 1px;
  136. }
  137.  
  138. /* перебиваем стили DTF */
  139. .sidebar__main {
  140. display: flex;
  141. flex-direction: column;
  142. flex-shrink: 0;
  143. min-width: 1px;
  144. overflow: auto;
  145. }
  146.  
  147. .sidebar-item {
  148. flex-shrink: 0;
  149. }
  150.  
  151. .sidebar-editor-button {
  152. margin-top: 24px;
  153. }
  154.  
  155. .sidebar-editor-buttons {
  156. margin-top: auto;
  157. margin-bottom: 16px;
  158. }
  159.  
  160. .account-menu {
  161. visibility: hidden;
  162. }
  163.  
  164. body.dtf-subs-script-inited .account-menu {
  165. visibility: visible;
  166. }
  167. `;
  168.  
  169. document.head.insertAdjacentHTML("beforeend", `<style type="text/css" id="dtfSubsStyles">${styles}</style>`)
  170. };
  171.  
  172. const fetchSubs = async (userId) => {
  173. const resp = await fetch(`https://api.dtf.ru/v2.5/subsite/subscriptions?subsiteId=${userId}`);
  174. const { result } = await resp.json();
  175.  
  176. return result.items;
  177. }
  178.  
  179. const getImageUrl = (uuid) => `https://leonardo.osnova.io/${uuid}/-/scale_crop/32x32/`;
  180.  
  181. const createSidebarItem = (name, imageId, href) => {
  182. const imgEl = cn('img', { class: 'icon', src: getImageUrl(imageId) });
  183. const nameEl = cn('span', {}, [name]);
  184. const result = cn('a', { class: 'sidebar-item _sub', href: transliterate(href), alt: name }, [imgEl, nameEl]);
  185.  
  186. return result;
  187. };
  188.  
  189. const createSidebarList = (items) => {
  190. const sidebarItems = items.map((item) => {
  191. const href = item.uri || `/u/${item.id}-${item.name.toLowerCase()}`;
  192.  
  193. return createSidebarItem(item.name, item.avatar.data.uuid, href);
  194. });
  195.  
  196. const title = cn('div', { class: 'sidebar-sibs__title' }, ['Подписки:']);
  197.  
  198. const listWrapper = cn('div', { class: 'sidebar-sibs__list modal-window__content' }, sidebarItems);
  199.  
  200. return cn('div', { class: 'sidebar-subs' }, [title, listWrapper]);
  201. };
  202.  
  203. const getProfileUrl = async () => {
  204. const userButton = await getDomElementAsync('.user');
  205. userButton.click();
  206.  
  207. const profileMenuItem = await getDomElementAsync('.account-menu__user-card');
  208. userButton.click();
  209.  
  210. return profileMenuItem.href;
  211. }
  212.  
  213. const getUserId = async () => {
  214. const profileUrl = await getProfileUrl();
  215. const userId = profileUrl.split('/u/')[1].split('-')[0];
  216.  
  217. return userId || null;
  218. };
  219.  
  220. const injectSubscriptions = async () => {
  221. const userId = await getUserId();
  222.  
  223. document.body.classList.add('dtf-subs-script-inited');
  224.  
  225. if (!userId) {
  226. return;
  227. }
  228.  
  229. const subs = await fetchSubs(userId);
  230. const list = createSidebarList(subs);
  231.  
  232. const sidebarButton = await getDomElementAsync('.sidebar-editor-button');
  233. sidebarButton.after(list);
  234. };
  235.  
  236. const runAutoExpandComments = () => {
  237. const expandComments = async () => {
  238. const expandCommentsButton = await getDomElementAsync('.comments-limit__expand');
  239. expandCommentsButton?.click();
  240. };
  241.  
  242. observeUrlChange(expandComments);
  243.  
  244. expandComments();
  245. };
  246.  
  247. const init = async () => {
  248. injectStyles();
  249. injectSubscriptions();
  250. runAutoExpandComments();
  251. };
  252.  
  253.  
  254. init();
  255. })();